In the last two posts (here and here) we added client-side validation support for the ValidatePasswordLengthAttribute that is part of the default ASP.NET MVC project template. In this post we will look at another of the validators in the template: PropertiesMustMatchAttribute. This attribute is used on RegisterModel and ChangePasswordModel to ensure that the user has entered the same password for both the Password and ConfirmPassword fields.

If you apply the steps from the previous post to the PropertiesMustMatchAttribute you may be surprised to discover that the client-side validator doesn’t get wired up. If you take a look at the source for ChangePasswordModel below, you will notice that ValidatePasswordLength is applied to the NewPassword property whereas PropertiesMustMatch is applied to the ChangePasswordModel itself. In other words, the ValidatePasswordLength is a proprety validator but PropertiesMustMatch is a type validator. This difference is significant as ASP.NET MVC only supports client-side validation for property validators.

In the remainder of this post we will create a MustMatchAttribute. This will be applied to a property (as shown below) to specify that its value should match another property, and will support client-side validation:

Here we are applying the MustMatch validator to ConfirmPassword and specifying that the property’s value must must the Password property. As in the previous posts, we will start by creating the client-side validation code:

This code is similar to the code from the previous post. The only real points of note are the use of the context property in the validation function to access the HTML form and the use of the jQuery $get function to find the property that we’re matching against.

With the client-side code taken care of the next step is to create the Validation attribute. Previously we derived from ValidationAttribute and supplied an override for the IsValid method. IsValid has the following signature:

publicoverridebool IsValid(object value)

The value parameter is the value for the item currently being validated. In the case of the type-level validator (PropertiesMustMatch) this is the instance of the ChangePasswordModel, which allows reflection to be used to access the properties to validate. However, in the case of a property-level validator such as the one we’re creating the value is the value of the property that the validator was applied to. This causes a problem as we need to be able to access other properties on the model. We hooked up an adapter class previously to provide the link to client-side validation – the adapter also exposes a validate method which gives us more context about the validation.

Note that we’re not expecting the IsValid method to be called as we’re going to intercept this in the adapter class. Just to be sure, we’re raising an exception to catch the situation where the adapter hasn’t been registered. Our adapter class will look as shown below:

the container parameter a reference to the object that contains the item being validated – in our example the instance of ChangePasswordModel

the Metadata property on DataAnnotationsModelValidator is an instance of ModelMetadata and gives us extra contextual information for the validation. Here we are using the ContainerType property to access the properties of the container and the Model property to access the value of the property that the attribute was applied to (this saves a property look-up via reflection)

The return type from Validate is an IEnumerable<ModelValidationResult> in contrast to the bool from ValidationAttribute.IsValid. Thanks to iterator support in C# we can use the yield return keyword to make a simple conversion to our code

Now that we have our adapter class we can register if with the framework:

So far we’ve created a replacement for the PropertiesMustMatch attribute that is applied to properties instead of classes. However, this currently only gives us server-side validation and the whole reason for this exercise was to add client-side validation! Fortunately we have got nearly all the pieces in place. Firstly we need a ModelClientValidationRule:

The GetFullHtmlFieldId takes the server-side property name and uses the ViewContext to get the qualified client-side id for the input field. This is then used by the client-side code to get the value that we’re comparing to.

With those last code modifications we can now use the MustMatch validator to ensure that our Password and ConfirmPassword fields contain the same value and have the validation applied on the client side.

In summary:

validation attributes can be applied to a class or property, but ASP.NET MVC only supports client-side validation for property-level validators

Property-level validators only have access to the property value through ValidationAttribute.IsValid but the adapter class contains a Validate method that give us greater context to work with

If this feels a little more complicated than you had hoped then stay tuned for the next post where we’ll see how this has been improved in ASP.NET MVC 3 Preview 1!

Thanks for a most fascinating post Stuart, I have really learned a lot here. I have one problem though, the MustMatch attribute is not working client-side. I suspect this is because of where I have placed the "Sys.Mvc.ValidatorRegistry" code. It is in the master page, after all the markup, but before the jQuery "$(function () {…}" startup code. Do you think this could be the problem? What can I do to diagnose this?

First of I'd check that your registration script is included, and also check that the rendered page that uses it includes the javascript to call through to your validator. After that I'd probably use a script debugger (the Developer Tools included in Internet Explorer 8 or Visual Studio work well) to check that the registration code fires and see whether your validator fires). It's often helpful at this point to temporarily include the debug versions of Microsoft.Ajax.

Well that explains most of it… I was wondering why the validation wasn't working correctly. One question, though: where is the best place to put the "Sys.Mvc.ValidatorRegistry.validators.mustMatch" function?

I'd put the function in a separate script file to make it easy to work with from a development perspective. It would then be worth looking at script minifiers/combiners to incorporate into the built process to improve the download performance.