Friday, October 21, 2011

Conditional Validation in ASP.NET MVC 3



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!