Alexander Manekovskiy

Automating Automation

Building Data Annotations Validator Control With Client-Side Validation

When I had worked on ASP.NET MVC project I really liked how input is validated with Data Annotations attributes. And when I had to return to the Web Forms, and write a simple form with some validation, I was wondering how I lived before with standard validator controls. For me, it was never convenient, when I had to write an enormous amount of server tags just to state that “this is required field which accepts only numbers in specified range…”. Yes, there is nothing terrible in declaration of two or three validation controls instead of one. But, if I had a choice, I would like to write only one validator per field and keep all input validation logic as far as I can from the UI markup. And being a developer the code-only approach is most natural for me.

System.ComponentModel.DataAnnotations namespace was introduced in .NET 3.5, and now its classes are supported by wide range of technologies like WPF, Silverlight, RIA Services, ASP.NET MVC, ASP.NET Dynamic Data but not in Web Forms. I thought that someone had already implemented ready-to-use Validator Control with client-side validation, but after searching the Web and most popular open source hosting services I found nothing. Ok, not nothing, but implementations what I have found lacked client-side validation and had some other issues. So I decided to write my own Data Annotations Validator that will also support client-side validation.

Creating Data Annotations Validator Control

Server-Side

As I wanted to achieve compatibility with existing validation controls (new validator is not a replacement for an old ones, it is just an addition to them), it was decided to inherit from BaseValidator. This class does all necessary initialization on both client- and server-sides and exposes all necessary methods for overriding.

1
2
public class DataAnnotationsValidator : BaseValidator
{ }

First of all EvaluateIsValid method of BaseValidator should be overridden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected override bool EvaluateIsValid()
{
  object value = GetControlValidationValue(ControlToValidate);
  foreach (ValidationAttribute validationAttribute in ValidationAttributes)
  {
      // Here, we will try to convert value to type specified on RangeAttibute.
      // RangeAttribute.OperandType should be either IConvertible or of built in primitive types
      var rangeAttibute = validationAttribute as RangeAttribute;
      if (rangeAttibute != null)
      {
          value = Convert.ChangeType(value, rangeAttibute.OperandType);
      }

      if (validationAttribute.IsValid(value)) continue;

      ErrorMessage = validationAttribute.FormatErrorMessage(DisplayName);
      return false;
  }

  return true;
}

The only interesting aspect of this method is line 16. I’m using FormatErrorMessage method of ValidationAttribute to use all the goodness like support of resources and proper default error message formatting. So, now there is no need to invent something with error messages.

Next thing to deal with is where to get ValidationAttributes collection. There is System.Web.DynamicData.MetaTable class that could be used to retrieve attributes. It was introduced in the first versions of ASP.NET Dynamic Data and now in 4.0 version of Dynamic Data, MetaTable has a static method CreateTable which accepts Type as input parameter. Why using MetaTable, why not retrieve attributes of Type directly from PropertyInfo for specified property name? Because MetaTable also supports retrieving of custom attributes that are applied to property through MetadataTypeAttribute and merges attributes applied to property both in metadata class and entity class. And again, why inventing something new when everything that is needed is right here?

1
2
3
MetaTable.CreateTable(ObjectType))
  .GetColumn(PropertyName).Attributes
  .OfType<ValidationAttribute>()

Now let’s look a bit into the future - if we place ObjectType property into DataAnnotationsValidator, it means that we should specify this property for every validator control on page. This is redundancy and leads to copy-pasting which is not acceptable. Lets step aside and create MetadataSource control that will act like metadata provider for validators on page.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MetadataSource : Control
{
  public Type ObjectType { get; set; }

  private MetaTable metaTable;
  private MetaTable MetaTable
  {
      get { return metaTable ?? (metaTable = MetaTable.CreateTable(ObjectType)); }
  }

  public IEnumerable GetValidationAttributes(string property)
  {
      return MetaTable.GetColumn(property).Attributes.OfType();
  }

  public string GetDisplayName(string objectProperty)
  {
      var displayAttribute = MetaTable.GetColumn(objectProperty).Attributes
          .OfType()
          .FirstOrDefault();

      return displayAttribute == null ? objectProperty : displayAttribute.GetName();
  }
}

Here I also thought about DisplayAttribute which is used to format default error message. Now how ObjectType of MetadataSouce should be specified? Well, we can do it programmatically on Page_Load or do it.. programmatically with CodeExpressionBuilder to keep all control setup in one place.

1
2
3
<dav:MetadataSource runat="server"
  ID="msFoo"
  ObjectType="<%$ Code: typeof(Foo) %>" />

Now, with existence of MetadataSource all fields of DataAnnotationsValidator are initialized in the OnInit method

1
2
3
4
5
6
7
8
9
10
11
12
protected override void OnLoad(EventArgs e)
{
  base.OnLoad(e);

  if (!ControlPropertiesValid())
      return;

  MetadataSource = this.FindChildControl(MetadataSourceID);

  ValidationAttributes = MetadataSource.GetValidationAttributes(ObjectProperty);
  DisplayName = MetadataSource.GetDisplayName(ObjectProperty);
}

This is all what was needed to provide server-side validation.

Client-Side

First of all, lets check what standard validator controls could be replaced with Data Annotations attributes.

Data Annotations Attribute Standard Validator Control
RequiredAttribute RequiredFieldValidator
StringLengthAttribute -
RegularExpressionAttribute RegularExpressionValidator
- CompareValidator
RangeAttribute RangeValidator

I have no ideas how to replace CompareValidator and I don’t think it is so critical and necessary to think on it. Time to look how standard validator controls are working on the client side.

Every validator that works on the client side should override AddAttributesToRender method of BaseValidator class. This method adds some fields to resulting javascript object. For example, RequiredFieldValidator adds evaluationfunction and initialvalue fields.

1
2
3
4
5
6
7
8
9
protected override void AddAttributesToRender(HtmlTextWriter writer) {
    base.AddAttributesToRender(writer);
    if (RenderUplevel) {
        string id = ClientID;
        HtmlTextWriter expandoAttributeWriter = (EnableLegacyRendering) ? writer : null;
        AddExpandoAttribute(expandoAttributeWriter, id, "evaluationfunction", "RequiredFieldValidatorEvaluateIsValid", false);
        AddExpandoAttribute(expandoAttributeWriter, id, "initialvalue", InitialValue);
    }
}

And resulting javascript block for RequiredFieldValidator will look next:

1
2
3
4
5
6
7
8
<script type="text/javascript">
//<![CDATA[
var rfvSampleTextBox = document.all ? document.all["rfvSampleTextBox"] : document.getElementById("rfvSampleTextBox");
rfvSampleTextBox.controltovalidate = "tbSampleTextBox";
rfvSampleTextBox.evaluationfunction = "RequiredFieldValidatorEvaluateIsValid";
rfvSampleTextBox.initialvalue = "";
//]]>
</script>

After examining source code of standard validator controls I found that every control sets evaluationfunction field that states a name for a javascript function that actually performs validation on the client-side. RequiredFieldValidator evaluation function is represented below.

1
2
3
function RequiredFieldValidatorEvaluateIsValid(val) {
    return (ValidatorTrim(ValidatorGetValue(val.controltovalidate)) != ValidatorTrim(val.initialvalue))
}

The val parameter is a validator object that was initialized with all the fields that were set in the AddAttributesToRender method. Plain and simple, if you need to supply your validator on client-side with some information override AddAttributesToRender and add what you want. To replace standard validators DataAnnotationsValidator is doing a little trick - it adds all standard evaluationfunction names, error messages and all necessary fields that are used by standard validation functions. Evaluation function of DataAnnotationsValidator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type="text/javascript">
//<![CDATA[
function DataAnnotationsValidatorIsValid(val) {
    var functionsToEvaluate = val.validatorFunctions.split(';;');
  var errorMessages = val.errorMessages.split(';;');

    for (var funcIndex in functionsToEvaluate) {
        var result = eval(functionsToEvaluate[funcIndex] + "(val)");
        if(result === false) {
          val.errormessage = errorMessages[funcIndex];
          val.innerText = errorMessages[funcIndex];
            return false;
        }
    }
    return true;
}
//]]>
</script>

This function is registered in the OnPreRender stage of control lifecycle.

1
2
3
4
5
6
7
8
9
10
11
12
13
protected override void OnPreRender(EventArgs e)
{
  base.OnPreRender(e);

  if (RenderUplevel)
  {
        var scriptManager = ScriptManager.GetCurrent(Page);
        if (scriptManager != null && scriptManager.IsInAsyncPostBack)
            ScriptManager.RegisterClientScriptResource(this, GetType(), DAValidationScriptFileName);
        else
          Page.ClientScript.RegisterClientScriptResource(GetType(), DAValidationScriptFileName);
  }
}

The only thing that remains is to get list of fields and values that are needed for validation functions. Every Data Annotation validation attribute will have an Adapter class that stores an array of ClientValidationRule classes. ClientValidationRule is just a container for storing javascript object field names and evaluationfunction.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClientValidationRule
{
    public string EvaluationFunction { get; set; }
    public Dictionary<string, object> Parameters { get; private set; }

    public string ErrorMessage { get; set; }

    public ClientValidationRule()
    {
        Parameters = new Dictionary<string, object>();
        EvaluationFunction = string.Empty;
    }
}

And ValidationAttributeAdapter acts like a bridge between existing ValidationAttibute and its ClientValidationRules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
internal class ValidationAttributeAdapter
{
    protected ValidationAttribute Attribute { get; set; }
    protected string DisplayName { get; set; }
    protected string ErrorMessage { get; set; }

    public ValidationAttributeAdapter(ValidationAttribute attribute, string displayName)
    {
        Attribute = attribute;
        DisplayName = displayName;
        ErrorMessage = Attribute.FormatErrorMessage(DisplayName);
    }

    public virtual IEnumerable<ClientValidationRule> GetClientValidationRules()
    {
        return Enumerable.Empty<ClientValidationRule>();
    }
}

All ValidationAttributeAdapter classes are registered within ValidationAttributeAdapterFactory in a Dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static readonly Dictionary<Type, Func<ValidationAttribute, string, ValidationAttributeAdapter>> PredefinedCreators
    = new Dictionary<Type, Func<ValidationAttribute, string, ValidationAttributeAdapter>>
    {
        {
            typeof(RangeAttribute),
            (attribute, displayName) => new RangeAttributeAdapter(attribute as RangeAttribute, displayName)
        }, {
            typeof(RegularExpressionAttribute),
            (attribute, displayName) => new RegularExpressionAttributeAdapter(attribute as RegularExpressionAttribute, displayName)
        }, {
            typeof(RequiredAttribute),
            (attribute, displayName) => new RequiredAttributeAdapter(attribute as RequiredAttribute, displayName)
        }, {
            typeof (StringLengthAttribute),
            (attribute, displayName) => new StringLengthAttributeAdapter(attribute as StringLengthAttribute, displayName)
        }
    };

public static ValidationAttributeAdapter Create(ValidationAttribute attribute, string displayName)
{
    Debug.Assert(attribute != null, "attribute parameter must not be null");
    Debug.Assert(!string.IsNullOrWhiteSpace(displayName), "displayName parameter must not be null, empty or whitespace string");

    // Added suport for ValidationAttribute subclassing. See http://davalidation.codeplex.com/workitem/695
    var baseType = attribute.GetType();
    Func<ValidationAttribute, string, ValidationAttributeAdapter> predefinedCreator;
    do
    {
        if (!PredefinedCreators.TryGetValue(baseType, out predefinedCreator))
            baseType = baseType.BaseType;
    }
    while (predefinedCreator == null && baseType != null && baseType != typeof(Attribute));

    return predefinedCreator != null
        ? predefinedCreator(attribute, displayName)
        : new ValidationAttributeAdapter(attribute, displayName);
}

As I said previously, idea was borrowed directly from ASP.NET MVC, so if you are familiar with its validation mechanisms you don’t need to learn how it works here. Approach here is almost identical to ASP.NET MVC which was described well by Brad Wilson. As in ASP.NET MVC DAValidation.IClientValidatable interface exposes an extension point and we can now create a custom validation attribute, implement IClientValidatable interface, write validation function or mix existing ones and get both server- and client-side validation. There is a great set of validation attributes - Data Annotations Extensions created by Scott Kirkland so it is only client function must be changed in order to use them with DataAnnotationsValidator.

1
2
3
4
public interface IClientValidatable
{
    IEnumerable<ClientValidationRule> GetClientValidationRules();
}

And that’s all, now we have fully functional control that makes validation with Data Annotations possible in the ASP.NET Web Forms universe.

Complete source code could be found on Codeplex. There you can download latest version of control and example project.

Comments