Conditional Validation in ASP.NET MVC 3


Update: If you like this, you’ll like Mvc.ValidationTookit even more – check out this post!

Some time ago I blogged on Conditional Validation in MVC, and Adding Client-Side Script to an MVC Conditional Validator. A number of people have asked me to update the sample to MVC 3, so guess what – it’s your birthday! The main differences are summarised below… and check out the code download to see it working. I’d recommend reading my previous two posts if you want the background on how it all works.

Important: Note that this is by no means a complete solution, and neither were my previous ones. They’re POCs intended to get you started! For example, you may need to handle different data types (Int32, perhaps) or control types (radio buttons, perhaps) yourself. I’d also recommend thoroughly testing the code. Enjoy…

The Attribute is the Adapter!

Hooray! There’s no need for an adapter class anymore. The Attribute instead can implement an interface;

 1: public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
 2: {
 3: }

IClientValidatable demands that we implement GetClientValidationRules on our Attribute, in a very similar way to the example in my previous posts. That’s much neater.

Our Context is Different

I’m not particularly happy with this workaround right now, so if you’ve a better solution let me know. The issue is that when we are emitting the Client Validation Rules described above, we must calculate the Identifier that the control we depend upon will have when it is written out into the HTML. To do this, we call;

 1: string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);

However… now that this method is being called from within the Attribute itself, rather than within an Adapter, that means it is executed while the field the Attribute applies to is being rendered. That means the context is one level lower than it was for my original solution. MVC tracks a “stack” of field prefixes in this context whilst rendering fields, and each time it ducks into a new template it adds a prefix. So when rendering our Person entity (which is a variable called model for example) the prefix would be “model_”. When rendering Person’s Name, it would be “model_Name”. And if we had an address field on Person, that in turn had a City field, it could become “model_Address_City”. Note the “model_” bit often isn’t there – it depends, and I’m simplifying Smile

What this means is that when calculating the dependent property ID with the code above, this context is “City” or “Country” (as the attribute is used on two fields it is called twice) rather than String.Empty. Which means it calculates the HTML ID of the “IsUKResident” field to be “City_IsUKResident”, instead of “IsUKResident”, and of course “City_IsUKResident” doesn’t exist. Therefore I have to post-process it to strip off “City_”.

Yuck. Anyone know how to navigate the prefix hierarchy to stop this happening? It does seem to work though.

jQuery Validation

Next up, we use the jQuery.validate library to write our validation functions. This is now the only option, instead of one of two options as it was before. My validator looks like this;

 1: $.validator.addMethod('requiredif',
 2:     function (value, element, parameters) {
 3:         var id = '#' + parameters['dependentproperty'];
 4:  
 5:         // get the target value (as a string, 
 6:         // as that's what actual value will be)
 7:         var targetvalue = parameters['targetvalue'];
 8:         targetvalue = 
 9:           (targetvalue == null ? '' : targetvalue).toString();
 10:  
 11:         // get the actual value of the target control
 12:         // note - this probably needs to cater for more 
 13:         // control types, e.g. radios
 14:         var control = $(id);
 15:         var controltype = control.attr('type');
 16:         var actualvalue =
 17:             controltype === 'checkbox' ?
 18:             control.attr('checked').toString() :
 19:             control.val();
 20:  
 21:         // if the condition is true, reuse the existing 
 22:         // required field validator functionality
 23:         if (targetvalue === actualvalue)
 24:             return $.validator.methods.required.call(
 25:               this, value, element, parameters);
 26:  
 27:         return true;
 28:     }
 29: );

You can see I’m just reusing the built in “required” validation if I determine it needs to be run.

However, we do also need to add an adapter that extracts the HTML 5 Custom Data Attributes that MVC adds to my input controls (use View Source and look for “data-XXX” on the <input> elements if you don’t know what these are – or read my article here) and passes them to my validation method. Mine looks like this;

 1: $.validator.unobtrusive.adapters.add(
 2:     'requiredif', 
 3:     ['dependentproperty', 'targetvalue'], 
 4:     function (options) {
 5:         options.rules['requiredif'] = {
 6:             dependentproperty: options.params['dependentproperty'],
 7:             targetvalue: options.params['targetvalue']
 8:         };
 9:         options.messages['requiredif'] = options.message;
 10:     });

This states that I require two parameters – dependentproperty and targetvalue. These are therefore passed in the options object for me. I must then do any processing on them that is required (none, in this case) and create an entry in options.rules with their processed values. I also need to ensure I put the error message into a dictionary indexed by the name of my validation rule. Phew! I’m sure that could be easier, couldn’t it? Perhaps I’ll write a little helper… there are helper functions for rules that only have a single parameter or need a boolean, but mine didn’t fit that pattern.

Enabling Validation

… is no longer required in the View, because it is enabled in Web.config instead, and is set to “true” by default! Excellent news!

Conclusion

Well, I hope that has been pleasantly brief. Download the code and have a play if you’re interested, and let me know how you get on. I think MVC 3 is a giant leap towards far better validation… and to celebrate that the sample uses Razor, of course!

Comments (27)

  1. Aaron Clausen says:

    Hi Simon,

    Many thanks for your great work on this. It is exactly what I've been looking for.

    Hopefully in MVC4 they can address some of the badly lacking validation features, although MVC3 is a fantastic start and looking good.

    With your solution, I'm having some issues on the client-side however as I need to check the value of a dropdown (int), not a bool. Does the supplied solution only cater for bools?

    Also, I've tried to apply multiple RequiredIf attributes to a property, but I get an error saying I'm only allowed to have one. But I need to say RequiredIf the dropdown's value is 1 or 2 or 3 and with a different message for each. Do you know how this logic could be implemented with your solution above?

    I may try to make some additions to your solution to suit my needs and if I have any lucky, I will post back another update. But looking forward to your thoughts.

    Many thanks.

  2. Aaron Clausen says:

    Hi again Simon,

    Just a big thank you. I have employed your code and spent all day playing with it and it works really awesome actually! Really saved me some time and it is working great.

    I figured out that if I need to apply multiple "RequiredIf" attributes to a single property, then I'd have to make the routine accept an array or something like that, i.e. just a bit fancier. But for now I can survive no problems in most scenarios.

    Saved me heaps of time, thanks again.

    Aaron.

  3. Simon J Ince says:

    @ Aaron;

    great to hear you've got it working!

    As for the integer data type, you're right that you may need to tweak the JavaScript; I certainly didn't test it with that type. One day I hope to turn this into a reusable library but for now time is against me 🙂

    Simon

  4. Nilesh says:

    Hi Simon,

    How would you achieve this in case of a complex model?

    We have a "Person" model with property "Address" of type "Address" class.

    public class Address

    {

      [RequiredIf("IsUKResident", true, ErrorMessage = "You must specify the City if UK resident")]

      public string City {get; set;}

    }

    where IsUKResident is a property of Person model class.

    Thanks

  5. Simon J Ince says:

    @ Nilesh;

    that's not something I'd considered. I think you'd need to do a split on "." with the target property name and try to navigate the object heirarchy. I don't see why that wouldn't be possible, so good luck!

    Simon

  6. Jason says:

    Simon,

    First off thanks for the tutorial.  It saved me alot of time.  

    I've included all your code in one of my projects including the jquery plugin.  The RequiredIf attribute works but it only validates server side and not client side.  Any thoughts on what I might be doing wrong?  

    Thanks

  7. Simon J Ince says:

    @ Jason;

    it depends 🙂

    1. If you're using any type of controls other than those in my sample you may need to add handling for them in the script; e.g. drop-down lists, radio buttons, etc. Equally data types I haven't handled may be converting incorrectly (e.g. numeric types).

    2. It could be missing script references – make sure all the jQuery and MVC-jQuery scripts are loaded before your conditional validation script. Try using the IE8 or FireFox+FireBug tools to debug your script and it should give you some pointers.

    HTH

    Simon

  8. dwick says:

    It looks like ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(string) simply appends TemplateInfo.HtmlFieldPrefix to whatever string you specify regardless if it actually exists or not; am i correct in this assumption?

  9. dwick says:

    I'm having success with…

    string depProp = context.Controller.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);

    @Nilesh, it works for your example as well as the opposite..

    public class Person

    {

     [RequiredIf("Address.Country", "USA", ErrorMessage = "SSN required for US citizens.")]

     [RegularExpression(@"^d{9,9}$", ErrorMessage = "Numbers only please.")]

     public string SSN { get; set; }

     public Address Address { get; set; }

    }

    Dave

  10. Simon J Ince says:

    @ David,

    I believe that' exactly what it does…

    Simon

  11. Emmanuel Buah says:

    Thanks for the code Simon. I extended your conditional validation to create a RangeIf validation attribute –emmanuelbuah.wordpress.com/…/conditional-validation-in-asp-net-mvc-rangeif-2  and hope to add more. Thanks for the direction. Let me know if my code could be improved.  

  12. Simon J Ince says:

    @ Emmanuel;

    just had a quick read and that's a nice post – I like how you turn it into a reusable "propertydependencyrule".

    Simon

  13. Brandon says:

    Hello. I'm just getting started with MVC3, so bear with me. Where does the string depProp live? Is it within the RequiredIfAttribute class?

    TIA

  14. Paul Heasley says:

    I haven't bothered implementing client side validation but here's an alternative way of doing conditional validation in MVC3 using IValidatableObject. http://www.phdesign.com.au/…/conditional-validation-in-asp-net-mvc3

  15. Simon J Ince says:

    @ Paul,

    i guess the problem with IValidatableObject is that you pretty much *cannot* make it run on the client/browser ever (please shout if someone disagrees, I've tried a few approaches at putting together a callback a la RemoteAttribute but none I liked so have given up)… which is why I always try to use the approach I've described above.

    Simon

  16. Simon J Ince says:

    @ Brandon,

    I'd recommend downloading the code – it will be much clearer. You can see the download just at the foot of the Conclusion – it is named Mvc3ConditionalValidation.zip

    Simon

  17. Jonathan H. says:

    Thanks for this, unfortunately I can't seem to get it to work with an integer.

           [RequiredIf("HasCategory", true, ErrorMessage = "A Category is required")]

           public int CategoryId { get; set; }

           public bool HasCategory{ get; set; }

    I'm using that in my view with a dropdown:

    @Html.DropDownListFor(model => model.CategoryId , new SelectList(Model.Category, "Id", "Name"), "–Select Category–")

                       @Html.ValidationMessageFor(model => model.CategoryId)

    Unfortunately, that doesn't seem to work.

    I have no other annotation on my int but the dropdown is built with all the other validations:

    <select id="CategoryId" name="CategoryId" data-val-requiredif-targetvalue="true" data-val-requiredif-dependentproperty="chkCompHasAudio" data-val-requiredif="A Status is required" data-val-required="The CategoryId field is required." data-val-number="The field CategoryId must be a number." data-val="true">

    Would really appreciate some help on this 🙂

    Thanks.

  18. Simon J Ince says:

    @ Jonathan,

    most likely is it is failing to handle the integer type… I didn't test for many data types etc; I "left that to the reader" (i.e. don't have time to perfect it!)

    Let me know how you get on.

    Simon

  19. Anthony says:

    Cheers for the post, its helped me heaps!

    I extended your implementation and did a blog post on it. It works the same except that it accepts multiple parameters instead of just a single one.

    anthonyvscode.com/…/mvc-3-requiredif-validator-for-multiple-values

    let us know what you think

  20. shawn z says:

    Cutting and pasting that does not work in MVC3. To get the extension to work, I had to create a class file:

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Web;

    using System.Web.Mvc;

    namespace incMvcSite.Classes {

       public static class HtmlPrefixScopeExtensions {

           public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix) {

               return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);

           }

           private class HtmlFieldPrefixScope : IDisposable {

               private readonly TemplateInfo templateInfo;

               private readonly string previousHtmlFieldPrefix;

               public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix) {

                   this.templateInfo = templateInfo;

                   previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;

                   templateInfo.HtmlFieldPrefix = htmlFieldPrefix;

               }

               public void Dispose() {

                   templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;

               }

           }

       }

    }

    In the Razor (.cshtml) file, I added the following:

    @using incMvcSite.Classes

    @using(Html.BeginHtmlFieldPrefixScope("Permission")) {

           Permission

           // The Html.EditorFor's would go here…

    }

    Notice the using to bring me extension class into scope. That allows the second using line to work.

    Now the problem is that when posting back, the object is not updated. In my controller, I used a second parameter to specify my prefix:

    TryUpdateModel(modelUser.Permission, "Permission");

    This added the prefix to all field in the HTML, and the TryUpdateModel loaded the object with prefixed control names. Now you can properly namespace your controls for embedded edit lists, and for partial views of models with the same property names.

    Shawn Zernik

    Internetwork Consulting

    http://www.internetworkconsulting.net

  21. Simon J Ince says:

    @ Shawn,

    Copy and pasting what doesn't work? The code download was written in MVC 3 so should work fine… and I'm not sure how your code fits in with this post?

    Simon

  22. Great Post!  I've implemented and it works well.  The only problem I'm having is that the client side validation doesn't work with jquery-1.6.2.  It works fine with 1.5.1, but on my current project I have to use 1.6.2.  I poked around, but nothing stood out as the cause.  Any ideas?

    Thanks,

    Mike

  23. Sebastien Lachance says:

    This is what I call a life saver!!! Thank you so much!

  24. Ashraful Alam says:

    Great work! I've improved the code to support it in complex value object – Person -> Address -> Region Required if Country is USA:

             if (depProp.Contains(thisField))

               // strip it off again

               {

                   depProp = depProp.Remove(depProp.IndexOf(thisField), thisField.Length);

               }

               }

  25. James Baker says:

    Great Post!  I've taken the time to clean up some of the JavaScript and handle multiple form element types and have tested it.  

    Here's a Gist for the JavaScript:

    gist.github.com/…/5409167

  26. Chit Min Maung says:

    for me, It's not working on Edit mode even it's working successfully on New mode.

  27. Andrew Beaven says:

    Clientside validation will fail if you're using nested EditorFor helpers with this code. That's probably what @Shawn was referring to.

    The id of the dependent property will be prefixed with the higher property names and this line here: if (depProp.StartsWith(thisField)) will always be false. As such, the dependent property passed through as validation parameters will be incorrect and will likely throw an exception when it tries to inspect the value of the dependent control.

    Ahh – looks like @AshrafulAlam has the fix.