OdeToCode IC Logo

Custom Data Annotation Validator Part II: Client Code

Wednesday, February 23, 2011

The validator we looked at in the last post only supports server side validation. To support client side validation we need a little more code on the server, and some custom client script.

One approach to client validation is to have the validation attribute implement the IClientValidatable interface, which requires a single method: GetClientValidationRules.

public class DateGreaterThanAttribute : 
                    ValidationAttribute, 
                    IClientValidatable 
{

    public IEnumerable<ModelClientValidationRule>
        GetClientValidationRules(ModelMetadata metadata,
                                 ControllerContext context)
    {
        var rule = new ModelClientValidationRule();
        rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
        rule.ValidationParameters.Add("other", OtherProperty);
        rule.ValidationType = "greaterdate";
        yield return rule;
    }

    // ...

 

The method returns an object containing all the data we need on the client. The runtime will serialize this data into HTML attributes on the target input element. Data includes the name of the "other" property, the name of the adapter the client needs for deserializing the data, and the error message to display in case of validation failure. Rendered output would look something like:

<input data-val="true" 
        data-val-greaterdate="EndDate must be greater than StartDate" 
        data-val-greaterdate-other="StartDate" 
        ... />

Note: the validation attribute went from being a "GreaterThan" attribute working against any IComparable to a "DateGreaterThan" attribute. In .NET, it's easy to compare integers, longs, DateTimes and decimals using the same code as they all implement IComparable. In JavaScript the situation is a bit thornier, so to avoid the messy parsing we'd need to deduce the type of the underlying data, we'll just stick with dates.

Client Script

Here is one way to implement the client script needed for validation:

/// <reference path="jquery-1.4.4-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />

jQuery.validator.unobtrusive
      .adapters.addSingleVal("greaterdate", "other");

jQuery.validator.addMethod("greaterdate",
    function (val, element, other) {
        var modelPrefix = element.name.substr(
                            0, element.name.lastIndexOf(".") + 1)
        var otherVal = $("[name=" + modelPrefix + other + "]").val();
        if (val && otherVal) {
            if (Date.parse(val) <= Date.parse(otherVal)) {
                return false;
            }
        }
        return true;
    }
);

(Update: should have been searching by name, not #id).

The first line is registering an adapter. An adapter is the code that pulls data from the HTML attributes and turns them into something jQuery.validate will understand. All the hard work is taken care of in the unobtrusive extensions provided by MVC 3. All we need to do is tell the extensions we need a single valued adapter, and the single value to look for is the "other" value (so the adapter will look inside the data-val-greaterdate-other attribute). Note the names we use on the server side ("greaterdate" and "other") have to match the adapter name and parameter name in script.

The last block of code registers the actual function used by jQuery.validate when the time comes to validate. You have to be careful if you are using model prefixes on the client side - the other property name might be "StartDate", but the input for editing StartDate has the name "Foo.StartDate". If you assume the source input uses the same model prefix as the other input, then the substr manipulation in the above code will find and use the proper prefix when locating the other input (it also works when there is no model prefix).

How do you know if the function will work? We'll apply some qUnit tests in the next post.