ASP.NET MVC 2 Custom Validation

archived comments edit

UPDATE: I’ve updated this post to cover changes to client validation made in ASP.NET MVC 2 RC 2.

This is the third post in my series ASP.NET MVC 2 Beta and its new features.

  1. ASP.NET MVC 2 Beta Released (Release Announcement)
  2. Html.RenderAction and Html.Action
  3. ASP.NET MVC 2 Custom Validation

In this post I will cover validation.

storage.canoe No, not that kind of validation, though I do think you’re good enough, you’re smart enough, and doggone it, people like you.

Rather, I want to cover building a custom validation attribute using the base classes available in System.ComponentModel.DataAnnotations. ASP.NET MVC 2 has built-in support for data annotation validation attributes for doing validation on a server. For details on how data annotations work with ASP.NET MVC 2, check out Brad’s blog post.

But I won’t stop there. I’ll then cover how to hook into ASP.NET MVC 2’s client validation extensibility so you can have validation logic run as JavaScript on the client.

Finally I will cover some of changes we still want to make for the release candidate.

Of course, the first thing I need is a contrived scenario. Due to my lack of imagination, I’ll build a PriceAttribute that validates that a value is greater than the specified price and that it ends in 99 cents. Thus \$20.00 is not valid, but \$19.99 is valid.

Here’s the code for the attribute:

public class PriceAttribute : ValidationAttribute {
  public double MinPrice { get; set; }

  public override bool IsValid(object value) {
    if (value == null) {
      return true;
    }
    var price = (double)value;
    if (price < MinPrice) {
      return false;
    }
    double cents = price - Math.Truncate(price);
    if(cents < 0.99 || cents >= 0.995) {
      return false;
    }

    return true;
  }
}

Notice that if the value is null, we return true. This attribute is not intended to validate required fields. I’ll defer to the RequiredAttribute to validate whether the value is required or not. This allows me to place this attribute on an optional value and not have it show an error when the user leaves the field blank.

We can test this out quickly by creating a view model and applying this attribute to the model. Here’s an example of the model.

public class ProductViewModel {
  [Price(MinPrice = 1.99)]
  public double Price { get; set; }

  [Required]
  public string Title { get; set; }
}

And let’s quickly write a view (Index.aspx) that will display an edit form which we can use to edit the product.

<%@ Page Language="C#" Inherits="ViewPage<ProductViewModel>" %>

<% using (Html.BeginForm()) { %>

  <%= Html.TextBoxFor(m => m.Title) %>
    <%= Html.ValidationMessageFor(m => m.Title) %>
  <%= Html.TextBoxFor(m => m.Price) %>
    <%= Html.ValidationMessageFor(m => m.Price) %>

    <input type="submit" />
<% } %>

Now we just need a controller with two actions, one which will render the edit view and the other which will receive the posted ProductViewModel. For the sake of demonstration, these methods are exceedingly simple and don’t do anything useful really.

[HandleError]
public class HomeController : Controller {
  public ActionResult Index() {
    return View();
  }

  [HttpPost]
  public ActionResult Index(ProductViewModel model) {
    return View(model);
  }
}

We haven’t enabled client validation yet, but let’s see what happens when we view this page and try to submit some values.

price-invalid

As expected, it posts the form to the server and we see the error messages.

Making It Work In The Client

Great, now we have it working on the server, but how do we get this working with client validation?

The first step is to reference the appropriate scripts. In Site.master, I’ve added the following two script references.

<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcValidation.js" type="text/javascript"
></script>

The next step is to enable client validation for the form by calling EnableClientValidation before****we call BeginForm. Under the hood, this sets a flag in the new FormContext which lets the BeginForm method know that client validation is enabled. That way, if you set an id for the form, we’ll know which ID to use when hooking up client validation. If you don’t, the form will render one for you.

<%@ Page Language="C#" Inherits="ViewPage<ProductViewModel>" %>

<% Html.EnableClientValidation(); %>
<% using (Html.BeginForm()) { %>

  <%= Html.TextBoxFor(m => m.Title) %>
    <%= Html.ValidationMessageFor(m => m.Title) %>
  <%= Html.TextBoxFor(m => m.Price) %>
    <%= Html.ValidationMessageFor(m => m.Price) %>

    <input type="submit" />
<% } %>

If you try this now, you’ll notice that the Title field validates on the client, but the Price field doesn’t. We need to take advantage of the validation extensibility available to hook in a client validation function for the price validation attribute we wrote earlier.

The first step is to write a ModelValidator associated with the attribute. Since the attribute is a data annotation, I can simply derive from DataAnnotationsModelValidator<PriceAttribute>like so.

public class PriceValidator : DataAnnotationsModelValidator<PriceAttribute> 
{
  double _minPrice;
  string _message;

  public PriceValidator(ModelMetadata metadata, ControllerContext context
    , PriceAttribute attribute)
    : base(metadata, context, attribute) 
  {
    _minPrice = attribute.MinPrice;
    _message = attribute.ErrorMessage;
  }

  public override IEnumerable<ModelClientValidationRule>   GetClientValidationRules() 
  {
    var rule = new ModelClientValidationRule {
      ErrorMessage = _message,
      ValidationType = "price"
    };
    rule.ValidationParameters.Add("min", _minPrice);

    return new[] { rule };
  }
}

The method GetValidationRules returns an array of ModelClientValidationRule instances. Each of these instances represents metadata for a validation rule that is written in JavaScript and will be run in the client. This is purely metadata at this point and the array will get converted into JSON and emitted in the client so that client validation can hook up all the correct rules.

In this case, we only have one rule and we are calling its validation type “price”. This fact will come into play later.

The next step is for us to now register this validator. Since we wrote this as a Data Annotations validator, we can register it in Application_Start as demonstrated by the following code snippet. If you you’re using another model validation provider such as the one for the Enterprise Library’s Validation Block, it might have its own means of registration.

protected void Application_Start() {
  RegisterRoutes(RouteTable.Routes);
  DataAnnotationsModelValidatorProvider
    .RegisterAdapter(typeof(PriceAttribute), typeof(PriceValidator));
}

At this point, we still need to write the actual JavaScript validation logic as well as the hookup to the JSON metadata. For the purposes of this demo, I’ll put the script inline with the view.

<script type="text/javascript">
  Sys.Mvc.ValidatorRegistry.validators["price"] = function(rule) {
    // initialization code can go here.
    var minValue = rule.ValidationParameters["min"];

    // we return the function that actually does the validation 
    return function(value, context) {
      if (value > minValue) {
        var cents = value - Math.floor(value);
        if (cents >= 0.99 && cents < 0.995) {
          return true; /* success */
        }
      }

      return rule.ErrorMessage;
    };
  };
</script>

Now when I run the demo, I can see validation take effect as I tab out of each field. Note that to get the required field validation to fire, you’ll need to type something in the field and then clear it before tabbing out.

Let’s pause for a moment and take a deeper look at what’s going on the code above. At a high level, we’re adding a client validator to a dictionary of validators using the key “price”. You may recall that “price” is the validation type we defined when writing the PriceValidator. That’s how we hook up this client function to the server validation attribute.

You’ll notice that the function we add to the validators itself returns a function which does the actual validation. Why is there this seemingly extra level of indirection? Why not simply add a function that does the validation directly to the dictionary?

This approach allows us to run some initialization code at the time the validator is being hooked up to the metadata (as opposed to every time validation occurs). This is helpful if you have expensive initialization logic. The validate method of the object we return in that initialization method may get called multiple times when the form is being validated.

Notice that in this case, the initialization code grabs the min value from the ValidationParameters dictionary. This is the same dictionary created in the PriceValidator class, but now living on the client.

We then run through similar logic as we did in the server side validation code. The difference here is we return null to indicate that no error occurred and we return an array of error messages if an error occurred.

Validation using jQuery Validation?

Client validation in ASP.NET MVC is meant to be extremely extensible. At the core, we emit some JSON metadata describing what fields to validate and what type of validation to perform. This makes it possible to build adapters which can hook up any client validation library to ASP.NET MVC.

For example, if you’re a fan of using jQuery it’s quite easy to use our adapter to hook up jQuery Validation library to perform client validation.

First, reference the following scripts.

<script src="/Scripts/jquery-1.4.1.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.js" type="text/javascript"></script>
<script src="/Scripts/MicrosoftMvcJQueryValidation.js" type="text/javascript">
</script>

When we emit the JSON in the page, we define it as part of an array which we declare inline. If you view source you’ll see something like this (truncated for brevity):

<script type="text/javascript"> 
//<![CDATA[
if (!window.mvcClientValidationMetadata) {
  window.mvcClientValidationMetadata = []; 
}
window.mvcClientValidationMetadata.push({
  {"Fields":[{"FieldName":"Title",...,
    "ValidationRules":
      [{"ErrorMessage":"This field is required",...,        "ValidationType":"required"}]},
   ...);
//]]>
</script>

Note the array named window.mvcClientValidationMetadata.

Simply by referencing the MicrosoftMvcJQueryValidation script, you’ve hooked up jQuery validation to that metadata. Both of the validation adapter scripts look for the existence of the special array and consumes the JSON within the array.

How about a demo?

And before I forget, here’s a demo application demonstrating the attribute described in this post.

Tags: aspnetmvc, validation, client validation, asp.net

Comments