Introduction to Validation
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 to the server. ASP.NET MVC2 comes with a flexible framework for applying validation using data annotations on the view model. Scott Gu has written an excellent tutorial on basic MVC validation here: http://weblogs.asp.net/scottgu/archive/2010/01/15/asp-net-mvc-2-model-validation.aspx.
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 (http://haacked.com/archive/2009/11/19/aspnetmvc2-custom-validation.aspx) 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 ValidationAttribute and 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));
Â
Example
The example code is here CrossFieldValidation.