How to Avoid Cut and Paste Code with ASP.NET MVC 2 Model Validation

Posted by on in Blogs
In this post, I will demonstrate how to make your own model validation attributes in order to share common validations throughout an ASP.NET MVC application, and which support MVC 2's client-side validation feature.

Validating a ZIP Code


As an example, consider a model for an address.

public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZIP { get; set; }
}


Now I would like to add a validation attribute in order to ensure that only valid ZIP codes can be entered. In the US, a valid ZIP code is either five digits, or five digits and then a hyphen and then four digits, e.g. 43082 or 43082-6999. I can use the built-in RegularExpressionAttribute for this:

public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
[RegularExpression(@"\d{5}(-\d{4})?", ErrorMessageResourceName = "InvalidZIPCode", ErrorMessageResourceType = typeof(MyApp.Resources))]
public string ZIP { get; set; }
}


This is enough to give me server-side validation when a model of this type is bound by the MVC framework. If I turn on client-side validation, then JavaScript will be generated to do the same validation on the client, as well.

//...
"ValidationParameters":{"pattern":"\\d{5}(-\\d{4})?"},"ValidationType":"regularExpression"}
// ..


So far, so good.

Don't Repeat Yourself


If this is the only ZIP code which ever appears in my application, I'm done. But if I have several ZIP codes in the application, I would like to consolidate this code into one place.

Perhaps, in a future release of the application, I will support Canadian postal codes in addition to US ZIP codes. I would like to be able to make this change in one place.

I can create a dedicated ZIP code validation attribute by subtyping the RegularExpressionAttribute.

[AttributeUsageAttribute(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class ZipCodeAttribute : RegularExpressionAttribute
{
public ZipCodeAttribute()
: base(@"\d{5}(-\d{4})?")
{
ErrorMessageResourceName = "InvalidZIPCode";
ErrorMessageResourceType = typeof(MyApp.Resources);
}
}


...and update the edit model to use it:

public class EditModel
{
public Guid Id { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string State { get; set; }
[ZipCode]
public string ZIP { get; set; }
}


Fixing Client-Side Validation


This looks good, and seems to work, but testing will reveal that MVC 2's client-side validation feature has stopped working, even though server-side validation will continue to work as expected. Examining the rendered JavaScript will show that the expected regular expression has not been injected into the page.

Internally, MVC stores a dictionary of attributes, and their corresponding client validation adapters. When it comes time to render the page, it goes through the list of attributes, and finds corresponding entries in the dictionary. Because of the way that this is implemented, it will not find adapters for subtypes of known attribute types. This seems like a violation of the Liskov Substitution Principle.

We can work around the problem, though. There is an API for adding additional attributes to the internal dictionary. In Global.asax.cs, you can add the following code to the Application_Start event handler:

private void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
// ...
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(ZipCodeAttribute), typeof(RegularExpressionAttributeAdapter));


The only comprehensive "documentation" for this that I'm aware of is the MVC source code.

After adding this code, client-side validation will start working again.


Comments

  • Guest
    mgroves Tuesday, 28 September 2010

    Very cool. The first part is straightforward, but the second part would have absolutely killed me if I hadn't read this post :)

  • Guest
    acorderob Thursday, 30 September 2010

    The "*" at the end of the regular expression should be a "?".

  • Guest
    Dave Thursday, 30 September 2010

    Dumb question, but I assume you would replace MyApp in the following line with the name of my application:
    ErrorMessageResourceType = typeof(MyApp.Resources);

    Any reason why Resources can't be found in my application namespace? A missing reference perhaps?

  • Please login first in order for you to submit comments
  • Page :
  • 1

Check out more tips and tricks in this development video: