MVC 2 Cross-field Validation

January 18th, 2010 by David Bending Leave a reply »

The built in annotation-based validation in MVC is fine, but sometimes you need to validate a property relative to other properties on the page.  For example, you might want to make sure that two properties are the same (password and confirm password) for example.

This post explains how to extend the validation model to cater for this.

First we need to create new validation attributes to annotate our data model with.  I use the following interface for all attributes that require more than one field to validate:

/// summary>
/// All validation data annotation attributes that require more than one
/// property to validate (or additional context) should implement this
/// interface.
// </summary>
public interface IRichValidationAttribute
{
    /// <summary>
    /// Determines whether the specified property is valid.
    /// </summary>
    /// <param name="controllerContext">Complete controller context (if required).</param>
    /// <param name="model">The model object to which the property belongs.</param>
    /// <param name="modelMetadata">Model metadata relating to the property holding
    /// the validation data annotation.</param>
    /// <returns>
    /// <c>true</c> if the specified property is valid; otherwise, <c>false</c>.
    /// </returns>
    bool IsValid(ControllerContext controllerContext, object model, ModelMetadata modelMetadata);
}

Now we can create a new attribute that implements this interface:

[AttributeUsage(AttributeTargets.Property |   AttributeTargets.Field, AllowMultiple = false)]
public class NotEqualToPropertyAttribute : ValidationAttribute, IRichValidationAttribute
{
   /// <summary>
    /// Gets or sets the other property we are validating against.
    /// </summary>
    public string OtherProperty { get; set; }

   /// <summary>
    /// Determines whether the specified value of the object is valid.
    /// </summary>
    /// <param name="value">
    /// The value of the specified validation object on which the attribute is declared.
    /// </param>
    /// <returns>
    /// true if the specified value is valid; otherwise, false.
    /// </returns>
    public override bool IsValid(object value)
    {
        // Work done in other IsValid
        return true;
    }

    /// <summary>
    /// Determines whether the specified property is valid.
    /// </summary>
    /// <param name="controller">Complete controller context (if required).</param>
    /// <param name="model">The model object to which the property belongs.</param>
     /// <param name="modelMetadata">Model metadata relating to the property holding the validation data annotation. </param>
     /// <returns>
     /// true if the specified property is valid; otherwise, false.
     /// </returns>
     public bool IsValid(ControllerContext controllerContext, object model, ModelMetadata modelMetadata)
     {
        if (model == null)
        {
             throw new ArgumentNullException("model");
        }
        // Find the value of the other property.
        var propertyInfo = model.GetType().GetProperty(OtherProperty);
        if (propertyInfo == null)
        {
           throw new InvalidOperationException(string.Format("Couldn't find {0} property on {1}.", OtherProperty, model));
        }
        var otherValue = propertyInfo.GetGetMethod().Invoke(model, null);
        return modelMetadata.Model.ToString() != otherValue.ToString();
    }
}

This can then be applied to the model:

[NotEqualToProperty(ErrorMessage = "Other occupation cannot be the same as the main occupation",
OtherProperty = "Occupation")]
public Occupation OtherOccupation { get; set; }

Now we need to create a validator for the attribute. I’ve created a base class for all validators than used the enhanced validation interface:

/// <summary>
/// A base class for validators that have attributes that require more
/// than one property to check for validity.
/// The standard <see cref="ValidationAttribute.IsValid"/> method
/// isn't sufficient because this method can't see the rest of the model.
/// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
public abstract class CrossFieldValidator<TAttribute> : DataAnnotationsModelValidator<TAttribute> where TAttribute : ValidationAttribute
{
    protected CrossFieldValidator(ModelMetadata metadata, ControllerContext context, TAttribute attribute) : base(metadata, context, attribute)
    {
    }
}


/// <summary>
/// Returns a list of validation error messages for the model.
/// </summary>
/// <param name="container">The container for the model.</param>
/// <returns>
/// A list of validation error messages for the model, or an empty list if no errors have occurred.
/// </returns>
public override IEnumerable<ModelValidationResult> Validate(object container)
{
var attribute = Attribute as IRichValidationAttribute;
if (attribute != null)
{
if (!attribute.IsValid(ControllerContext, container, Metadata))
{
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
else if (!Attribute.IsValid(container))
{
yield return new ModelValidationResult { Message = ErrorMessage };
}
}

Finally we need to create the client-side script:

jQuery.validator.addMethod("notequaltoproperty", 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;
});

And that’s it.

Share
Advertisement
Celtic-Style Contemporary Jewellery and Gifts

5 comments

  1. Michael Taylor says:

    Nice post!

    Doing this had the knock on effect of forcing me to use ViewModels; my LINQ to SQL model lives in a seperate Assembly and I didn’t want to add a reference to System.Web.Mvc to allow the new context aware attributes to work.

    I’m looking forward to the MVC Futures support for DataAnnotations 4 which support ValidationContext.

  2. As it stands I think it would. MVC2RC2 has changed things around a bit, so I’ll post an update once I’ve had time to digest the implications.

    _
    D

  3. Stefanvds says:

    Hey.

    thanks for your helpful post. This is very useful.

    however, i can’t get it to work. i don’t know where to put the code you placed under the sentence
    “Now we need to create a validator for the attribute. I’ve created a base class for all validators than used the enhanced validation interface:”

    especially the last part which doesn’t have a class around itself…

    i placed that part in the class on the top of that block, but it still doesnt work. the problem is that when i validate, it doesnt execute the code, but still executes the default isValid method…

  4. From memory that is part of the abstract validator class. However major caveat: I haven’t tried this code against MVC 2 RC 2 so I don’t know if it still works yet.

Trackbacks /
Pingbacks

  1. ASP.NET MVC Archived Buzz, Page 1

Leave a Reply