DataAnnotations, MVC 3, and Unobtrusive Validations

DataAnnotations are a pretty cool introduction to .NET 3.5+. It is very useful in ASP.NET MVC 3, and I’ve written a somewhat naive attempt to use this functionality in ASP.NET Web Forms which some people have found very useful.

I’d like to dig a little more in depth…

Ultimately, the problem with most resources for researching DataAnnotations and how they apply to things like jQuery.unobtrusive.validation and MVC 3 is that few of these references explain the connection between the server and the client. You can download the MVC 3 source code and figure out exactly what’s going on (this is why I’ve done).

The problem there is that not all developers have time to track the interaction between the model, parsing and building the page with *magic* HTML5 attributes, then relying on the jQuery validation plugin to parse those attributes on the client side and build an appropriate ruleset for validation which is validated before the form is submitted. It’s not complicated, but it’s also somewhat involved for anyone looking for a quick answer. I’ll try to make it a little easier.

First thing’s first: download the source code for MVC. Visit aspnet.codeplex.com and download the MS-PL version of the source code (the line says The MS-PL distribution of the source code can be downloaded here.). If you don’t want to track through the source code, that’s cool. It’s just as easy to read through and ignore line number references.

ModelMetadata

Open ModelMetadata.cs. You’ll notice at the end of this file (around line 409) that there is a GetValidators method which queries the static class ModelValidatorProviders‘s Providers property. If you open that class and look at the default providers, you’ll see:

We’re really only interested in the DataAnnotationsModelValidatorProvider. In this class at line 123, you’ll notice a RegisterAdapter method. This method is called from Global.asax in the ApplicationStart method, and is the heart of registration for our validations. So far, we know we need to understand ModelMetadata, DataAnnotationsModelValidatorProvider#RegisterAdapter, and ValidationAttribute.

ValidationAttribute

ValidationAttribute is the abstract base class which provides functionality for the following types:

  • System.ComponentModel.DataAnnotations.CustomValidationAttribute
  • System.ComponentModel.DataAnnotations.DataTypeAttribute
  • System.ComponentModel.DataAnnotations.RangeAttribute
  • System.ComponentModel.DataAnnotations.RegularExpressionAttribute
  • System.ComponentModel.DataAnnotations.RequiredAttribute
  • System.ComponentModel.DataAnnotations.StringLengthAttribute

If you look at line 47 of the DataAnnotationsModelValidatorProvider class, you’ll see that only the last 4 of these attributes have factories provided by default. This makes sense if you consider that you’re responsible for any custom attributes or custom data types. The dictionary created at line 47 is points each ValidationAttribute to the correct adapter.

Adapters

I’ve taken us the long way to get the to first major point of DataAnnotations in ASP.NET MVC: Adapters. This is because it’s important to know what’s going on to see how Adapters fit in. Now that we know the ModelMetadata queries the providers for any registered adapters, let’s see what’s so important about the adapters. Open the class RequiredAttributeAdapter. This class is a DataAnnotationsModelValidator for the RequiredAttribute. You can see there is one method, GetClientValidationRules, which returns an enumerable of ModelClientValidationRule. Currently, the MSDN documentation for this type has no real information.

This is where rulesets are created for rendering output to the client.

A ModelClientValidationRule specifies the validation type, error message, and parameters used on the client side for validation. For the RequiredAttribute, you’ll see the client validation rule is created with the type “required” and the error message set to whatever the attribute’s ErrorMessage property is.

That’s where things are ‘registered’. How do they get to the client?

HtmlHelper

The HtmlHelper class provides a method called GetUnobtrusiveValidationAttributes. Navigate to line 307 of HtmlHelper.cs and you’ll see how these client rules are transformed into HTML5 attributes. First, the rule’s type is turned into an attribute in the format:

data-val-type="message

So, for a RequiredAttribute with an error message set to “You must enter an email address”, this code will generate an attribute on your HTML element which looks like:

data-val-required="You must enter an email address

See, it’s pretty simple. Next, if the ValidationAttribute has any parameters to supply, it creates another attribute in the format:

data-val-type-param="value

If there are any attributes to be added to the element, another attributed is created: data-val=”true”. These attributes are ultimately merged into existing attributes with an element is created. Do a solution-wide search on this method if you’re interested in seeing how tags are created. If you’d like to see an example of using parameters, check out ModelClientValidationStringLengthRule, which sets the minimum and maximum length of a string, for example as data-val-min=”1″ and data-val-max=”250″.

Registering Adapters

We’re on our way to understanding how these attributes get from the model to the client. There are two important things to verify before registering adapters– the jquery.unobtrusive.validation settings! These are in web.config (or in code, see MVC 3 release notes):

<configuration>
        <appSettings>
            <add key="ClientValidationEnabled" value="true"/>
            <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
        </appSettings>
    </configuration>

Now that we’re sure the application is registered to create the default HTML5 unobtrusive validation attributes, let’s see how we’d register a custom adapter. We’ll use the example from MSDN for MVC 2, although it doesn’t explain unobtrusive validation. In Global.asax, you must register the adapter in the following way:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
    DataAnnotationsModelValidatorProvider.RegisterAdapter(
        typeof(RemoteUID_Attribute), 
        typeof(RemoteAttributeAdapter));
}

To see the implementation of RemoteUID_Attribute and RemoteAttributeAdapter, check out the first code snippet from that example. Of course, you should call RegisterAdapter with your ValidationAttribute-derived type and your adapter.

Now, when a page is delivered to the client, if you’ve annotated your model with your custom attribute, you should see the HTML5 data-val-* attributes that you’ve specified.

To the client!

Most of the magic on the client side happens in jquery.validate.unobtrusive.js.

If you open that file and scroll to the bottom, you’ll see that an adapter is added for the “required” type and a callback is provided that sets validation options on each matching element that mark it as required. As you can see from other adapter registration in this file, “required” is one of the simpler functions.

When an adapter is registered on the client side, it is added to the options object as a rule and an optional message for that rule. This options object is stored on an element’s parent form using jQuery .data(). So, your form will have a data object such as:

unobtrusiveValidation : {
  options: {
    rules: { },
    messages: { }
  }
}

There is a lot more to that object, but you get the gist. If you’d like to poke around at the object on a given page, open Google Chrome Developer tools or firebug and enter:

$('form:first').data('unobtrusiveValidation')

At the end of the file, you’ll see that a function is passed to the jQuery function. Many don’t know that this is equivalent to

$(document).ready(function() { });

So, jquery.validate.unobtrusive is parsing the entire document on page load.

As previously mentioned, it finds the data-val=”true” attribute and parses each element. To be sure, the selector used is “:input[data-val=true]”. parse calls parseElement, which is where the parent form’s data element is modified. Just as data-val-* attributes were built on the server, those attributes are being deconstructed on the client.

On validation, each element is checked for any rules. If a rule fails, the message is displayed in the validation summary. This is all done through the jquery.validate plugin, and is a topic for a future blog post!

Now, if you wanted to add a custom method to the jquery.validate validation chain, you’d have to call addMethod, providing a name, a method, and a message. Here’s the ‘gotcha’, though: YOU MUST NOT CALL addMethod ON DOCUMENT LOAD!

If you’d like to read further about using jquery.validate, check out the author’s page and the plugin’s documentation:

http://bassistance.de/jquery-plugins/jquery-plugin-validation/

http://docs.jquery.com/Plugins/Validation

An Example

I’ve created a full working example on github. Check it out here: https://github.com/jimschubert/ContainsAttributeExample

This example demonstrates how to turn the ContainsAttribute below into a client-side and server-side validation:

/// <summary>
/// The attribute
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
public sealed class ContainsAttribute : ValidationAttribute
{
    readonly string _contain;
    public ContainsAttribute(string contain)
    {
        this._contain = contain;
    }

    public string Part
    {
        get { return _contain; }
    }

    public override bool IsValid(object value)
    {
        if (value == null) return true; // RequiredAttribute will check for null.
        return value is string && System.Convert.ToString(value).Contains(_contain);
    }
}

So you don’t miss it, here is the working bit of JavaScript code for registering the client-side validation…

// NOTE: Immediate function, not onload.  These will not work in the onload function.
(function ($) {
    $.validator.addMethod('contains', function (value, element, param) {
        console && (typeof console.log === "function") && console.log('contains called');
        if (this.optional(element)) { return true; } // let required rule deal with this.

        var pattern = new RegExp('' + param, "gi");
        return value && ('' + value).match(pattern);
    }, "Part of the word is invalid");

    $.validator.unobtrusive.adapters.add('contains', function (options) {
        var element = options.element,
                message = options.message;
        options.rules['contains'] = $(element).attr('data-val-contains-word');
        if (options.message) {
            options.messages['contains'] = options.message;
        }
    });
})(jQuery);

// onload function
$(function () {
    $(':input[data-val-contains]').each(function () {
        $.validator.unobtrusive.parseElement(this, true);
    });
});

Related Articles