Extending ASP.NET MVC’s Validation
This week’s MSDN Flash feature article is all about enhancing the validation support in ASP.NET MVC using the specific example of cross-field validation. My thanks to David Bending for writing the article which is a little more “code-heavy” than a typical MSDN Flash article. What do you think? Would you prefer to see more of this style of article? Or maintain the status quo? Let me know by either contacting me here or sending me a tweet @MikeOrmond.
And of course if you have ideas for Flash articles you’d like to see or polls you’d like, er, “polled” or you have an idea for an article you’d like to write yourself then let me know as well. Now over to David…
Nearly all web applications require some form of validation. Validation performs two main purposes: helping the user to enter correct values on a web page, and protecting the application from invalid or malicious input.
Validation can take place client-side within the browser, or server-side when the page is posted. ASP.NET MVC2 comes with a flexible framework for applying validation using data annotations on the view model. Scott Guthrie has written an excellent tutorial on basic MVC validation here.
Limitations of Built-in MVC Validation
The built-in validation works well for standard scenarios, but there are a few common scenarios it can’t cope with. One of the main issues is that validation can only be done on a model property in isolation. Often we will want to validate a property based on the current value of some other property: for instance we may want our confirm password field value to be the same as the password box.
In this article we will extend the MVC validation framework to provide support for cross-field validation. We’ll do this in part by creating custom validation attributes as described in Phil Haack’s article but we’ll also need to extend the validator framework to allow us to validate across fields.
Extending the Framework to Support Cross-Field Validation
To create a custom validation attribute we usually derive from ValidationAttributeand override the IsValid method, however IsValid only gets passed the value of the property to validate and we are going to need to see the value of other fields in the model. To accommodate this we’ll create an interface ICrossFieldValidationAttribute that has an IsValid method with access to the entire view model (note these code snippets only show significant lines – download the example solution to get the whole files):
public interface ICrossFieldValidationAttribute
{
bool IsValid(ControllerContext controllerContext,
object model,
ModelMetadata modelMetadata);
}
Next we build a base class for cross-field validators. This base class uses the extended IsValid defined above, rather than the narrower method used in the framework’s DataAnnotationsModelValidator class.
public abstract class CrossFieldValidator<TAttribute> :
DataAnnotationsModelValidator<TAttribute>
where TAttribute : ValidationAttribute, ICrossFieldValidationAttribute
{
public override IEnumerable<ModelValidationResult> Validate(object container)
{
var attribute = Attribute as ICrossFieldValidationAttribute;
if (!attribute.IsValid(ControllerContext, container, Metadata))
{
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
Building the Validation
Now that we have a cross-field validation framework in place, we can build our new attribute.
[AttributeUsage(
AttributeTargets.Property |
AttributeTargets.Field, AllowMultiple = false)]
public class EqualToPropertyAttribute :
ValidationAttribute, ICrossFieldValidationAttribute
{
public string OtherProperty { get; set; }
public bool IsValid(ControllerContext controllerContext,
object model, ModelMetadata modelMetadata)
{
var propertyInfo = model.GetType().GetProperty(OtherProperty);
var otherValue = propertyInfo.GetGetMethod().Invoke(model, null);
if (modelMetadata.Model == null) modelMetadata.Model = string.Empty;
if (otherValue == null) otherValue = string.Empty;
return modelMetadata.Model.ToString() == otherValue.ToString();
}
}
And apply it to the view model:
public class Account
{
public string Password { get; set; }
[EqualToProperty(OtherProperty = "Password")]
public string ConfirmPassword { get; set; }
}
We also need to create a validator class to emit the correct client-side rules, and to invoke the IsValid method.
public class EqualToPropertyValidator :
CrossFieldValidator<EqualToPropertyAttribute>
{
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule
{
ValidationType = "equaltoproperty",
ErrorMessage = Attribute.FormatErrorMessage(Metadata.PropertyName),
};
rule.ValidationParameters.Add("otherProperty", Attribute.OtherProperty);
return new[] { rule };
}
}
Finally we need to create a JavaScript function to evaluate the rule client-side:
jQuery.validator.addMethod("equaltoproperty", function (value, element, params) {
if (this.optional(element)) {
return true;
}
var otherPropertyControl = $("#" + params.otherProperty);
if (otherPropertyControl == null) {
return false;
}
var otherPropertyValue = otherPropertyControl[0].value;
return otherPropertyValue == value;
});
function testConditionEqual(element, params) {
var otherPropertyControl = $("#" + params.otherProperty);
if (otherPropertyControl == null) {
return false;
}
var otherPropertyValue;
if (otherPropertyControl[0].type == "checkbox") {
otherPropertyValue = (otherPropertyControl[0].checked) ? "True" : "False";
} else {
otherPropertyValue = otherPropertyControl[0].value;
}
return otherPropertyValue == params.comparand;
}
And the final step is to register our new attribute with MVC. We do this in Global.asax Application_Start method like this:
DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(EqualToPropertyAttribute),
typeof(EqualToPropertyValidator));
Next Steps
Using the framework we’ve implemented here we can develop validation as complex as we need. Everything is testable using unit tests for the server-side validation and Visual Studio 2010 coded UI tests for the client-side validation.
Other approaches you could take would be to use the xVal validation framework from CodePlex, or download the preview of MVC 3 that contain a cross-field validation framework in the box, using a technique very similar to this article.
About the Author
David Bending is a Technical Lead at Confused.com, the UK’s first insurance price comparison website. David specialises in Agile development on the .NET platform and blogs about MVC, Agile and other topics at Davepoint.