Condividi tramite


Conditional Validation in MVC

Recently I put together samples for different types of validation for some customers, and one of those was Conditional Validation – that is “this field is required if another field is true”, and such like. But a few weeks later I saw my code fail! I got home and tried to reproduce it, and couldn’t…

So this blog post is a sorry tale of how I spent hours and hours trying to work around my own dumb mistake. Huge thanks go to Stuart who didn’t call me too many names for wasting both our time J

The Approach

Fundamentally the approach to getting conditional validation working is to use the Validator and the Attribute in tandem. Check out the docs for the DataAnnotationsModelValidator and ValidationAttribute classes if you’ve not heard of these.

The problem you initially come across with validating based on the content in another field is that the signature of the Attribute’s IsValid method looks like this;

 public override bool IsValid(object value)

Notice the absence of any reference to the rest of the entity being validated, or any validation context, so you can’t check the value of another field. Now MVC actually calls Validate on your Validator class, which in turn uses the attribute’s IsValid method to perform the validation, and you’ll be pleased to see that Validate has a much more useful signature;

 public override IEnumerable<ModelValidationResult> Validate(object container) { }

This actually receives the whole entity being validated. To get at extra information about it, and indeed the value currently under validation, we use the Metadata property on the validator’s base class. This means my Validate method ends up looking like this;

 public override IEnumerable<ModelValidationResult> Validate(object container)
{
   // get a reference to the property this validation depends upon
   var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
   if (field != null)
   {
      // get the value of the dependent property
      var value = field.GetValue(container, null);
      // compare the value against the target value
      if ((value == null && Attribute.TargetValue == null) ||
         (value != null && value.Equals(Attribute.TargetValue)))
      {
         // match => means we should try validating this field
         if (!Attribute.IsValid(Metadata.Model))
            // validation failed - return an error
            yield return new ModelValidationResult { Message = ErrorMessage };
      }
   }
}

The comments should enable you to follow the code fairly easily, but fundamentally we check the value of the Dependent Property to see whether we should perform our conditional validation. If we should, we call Attribute.IsValid.

This approach means it is essential to register your attribute and validator pair correctly in Global.asax;

 DataAnnotationsModelValidatorProvider.RegisterAdapter(
   typeof(RequiredIfAttribute), 
   typeof(RequiredIfValidator));

Easy as pie, huh? All you need to do now is create a custom attribute with DependentProperty and TargetValue properties and you’re sorted. Then you use it like this;

 public class ValidationSample
{
   [RequiredIf("PropertyValidationDependsOn", true)]
   public string PropertyToValidate { get; set; }

   public bool PropertyValidationDependsOn { get; set; }
}

Easy right?

Whoops!!!

Well, basically yes. That is all you need to do. But I made a tiny (well intentioned) mistake in my validation code that caused me to see intermittent errors. When MVC is performing Model Binding it has the following approximate flow;

  1. Create an instance of the model type that is required

  2. Loop through all properties

    1. Perform required field validation on the current property
    2. Try to set the value of a property from the value providers
    3. If that fails log an error in ModelState
  3. When all properties have been processed;

    1. Get a composite validator (that basically does as follows)

    2. Loop through all properties

      1. Get a list of validators

        1. Call validate on each one
        2. If validation fails log an error in ModelState

In other words, it performs “model validation” after model binding has completed, which means you know all properties have been set accordingly before you do any validation. This means our conditional validation works great.

Unless…

Unless you inherit from RequiredAttribute for your custom attribute. I originally did this as I wanted to reuse the IsValid logic built into RequiredAttribute. But as soon as I did that, the MVC framework treated my custom conditional validation as required field validation and performed it in step 2a in the flow above, not as part of the final model validation phase. The problem with this is that other properties are not guaranteed to have been set at this point, and sure enough that was what caused me problems. As an example, imagine a class declared as follows;

 public class Person
{
   public string Name { get; set; }

   [RequiredIf("IsUKResident", true)]
   public string City { get; set; }

   public bool IsUKResident { get; set; }
}

… and imagine my RequiredIfAttribute inherits from RequiredAttribute. In this scenario the validation won’t work. The logic states “validate that city is not empty if IsUKResident is true”. But the IsUKResident field is declared after the City field, so it hasn’t been model-bound when my validator is executed. The default value for a Boolean is false, so my validation that is conditional on this field being true will never get executed.

So, in summary…

The moral of the story; don’t inherit from RequiredAttribute unless this is the behaviour you want!

I’ve also attached an example application for you to play with that demonstrates working conditional validation. To see it fail as per the comments in this post, just change the implementation of RequiredIfAttribute to inherit from RequiredAttribute and delete the override of IsValid.

Originally posted by Simon Ince on June 4th 2010 here https://blogs.msdn.com/b/simonince/archive/2010/06/04/conditional-validation-in-mvc.aspx

ConditionalValidation.zip