Adding Client-Side Script to an MVC Conditional Validator
Update: If you like this, you'll like Mvc.ValidationTookit even more - check out this post!
In a previous post I covered how to write a conditional validator to work with ASP.NET MVC, and a little gotcha to avoid. However, I didn’t include any details on how to wire up this validator with some client side JavaScript. This post is tacks some script onto that approach! It wasn’t as simple as I expected, so if you have any comments please do chip in, and usual caveats apply.
Emitting Client Validation Rules
MVC uses the GetClientValidationRules override on your Validator class to provide details of the client side validation to run. Looking at my example you can see the following code;
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = ErrorMessage,
ValidationType = "requiredif",
};
var viewContext = (ControllerContext as ViewContext);
string depProp = viewContext
.ViewData
.TemplateInfo
.GetFullHtmlFieldId(Attribute.DependentProperty);
rule.ValidationParameters.Add("dependentProperty", depProp);
rule.ValidationParameters.Add("targetValue", Attribute.TargetValue);
yield return rule; }
This does a few interesting things – firstly it gives our client side validation type a unique identifier of “requiredif”; we’ll use that later. Secondly, it adds parameters that should be passed to the client script. In this case they are “dependentProperty” – the other field whose value must be checked – and “targetValue” – the value it must be for this validator to fire.
There is a little complexity here. We’ve specified the dependent property as a simple string, but of course that property may be getting rendered deep inside a template hierarchy in MVC, and therefore may well have prefixes. So a “CustomerName” field may really be rendered as an HTML field with the ID “Customers[3].Data.CustomerName”.
To get a fully qualified field name we call the GetFullHtmlFieldId method. Note the way I have accessed it, via the ViewContext.ViewData property. At first I spent some time debugging this code as I’d referenced it via ControllerContext.Controller.ViewData, which doesn’t work as it gets a ViewContext higher up the tree.
This results in some Json being emitted in the HTML page that starts with this line;
window.mvcClientValidationMetadata.push( {"Fields":[ { …….
The metadata Json now includes some data as follows that identifies one of our fields as needing “requiredif” validation;
"ValidationParameters" {
"dependentProperty":"Customer_CustomerName",
"targetValue":true
},
"ValidationType":"requiredif"
You can see the property name is fully qualified if needed.
The JavaScript Bit: Microsoft MVC Ajax
This is great, but we’re still not getting any script validation right? So what we need to do is define the requiredif validation rule so that the framework can apply it. Firstly, make sure that you’ve imported all the required JavaScript dependencies, and your own new script file (mine is called conditional-validation.js);
<script src="<%= Url.Content("~/Scripts/MicrosoftAjax.debug.js") %>"
type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/MicrosoftMvcAjax.debug.js") %>"
type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/MicrosoftMvcValidation.debug.js") %>"
type="text/javascript"></script>
<script src="<%= Url.Content("~/Scripts/conditional-validation.js") %>"
type="text/javascript"></script>
If you are using jQuery these files are different; the attached sample solution has both Microsoft and jQuery examples so check that out. Also make sure you call Html.EnableClientValidation before Html.BeginForm to get the Json metadata emitted as described previously.
Next, let’s define our own validation rule. When you’re using the Microsoft MVC validation framework this is done by adding the validator to the ValidatorRegistry using script as follows;
var validators = Sys.Mvc.ValidatorRegistry.validators; validators["requiredif"] = Function.createDelegate(
null,
Sample.RequiredIfValidator.create);
Recognise that “requiredif” unique identifier for our validation? It’s the same as we emitted from the server in our Client Validation Rule. The Sample.RequiredIfValidator.create function is a delegate that extracts rule parameters (our dependentProperty and targetValue) and passes them into the constructor for a class that implements a validate method;
Sample.RequiredIfValidator.create = function create(rule) {
var dependentProperty =
rule.ValidationParameters['dependentProperty'];
var targetvalue =
rule.ValidationParameters['targetValue'];
var instance = new Sample.RequiredIfValidator(
dependentProperty, targetvalue);
return Function.createDelegate(instance, instance.validate);
}
The return value from create is a delegate that points to the validate method. In the validate method we compare targetvalue against the value of the control that represents dependentProperty; if it matches (i.e. the condition is true) we want to execute required field validation.
Rather than rewrite the required field validation that we need if the condition succeeds and we want to perform the validation, I’ve reused that built into the framework, by getting an instance of a required validator from the Validator Registry and then executing it;
var validatorinstance = Sys.Mvc.ValidatorRegistry.validators.required();
return validatorinstance(value, context);
Nice huh?
The JavaScript Bit: jQuery
Now, if you’re using jQuery I find the syntax to create a custom validator a bit simpler. That is, you basically add a validation function to a list of methods;
$.validator.addMethod('requiredif',
function (value, element, parameters) {
// ... implementation
}
To reuse the built in required field validation, we use this syntax instead;
return $.validator.methods.required.call(
this,
value,
element,
parameters);
If I’ve skimmed over this a bit quickly have a look at the attached download (both jQuery and Microsoft implementations are in conditional-validation.js) and then reread this; all should become clear.
Something to Watch Out For
When you’re comparing the value held in the dependentProperty control against the targetValue emitted from the server, remember that the control will always return a string. Therefore I’ve done a little manipulation to ensure that the targetValue is always a string too;
var targetvalue = parameters['targetValue'];
targetvalue = (targetvalue == null ? '' : targetvalue).toString();
This means that the comparison behaves as expected. For example, if targetValue is true but actualValue was “true” (note the quotes) the comparison could have failed without this step. I thought JavaScript would coerce the types and get it right; but it didn’t.
Comments
Anonymous
June 11, 2010
"For example, if targetValue is true but actualValue was “true” (note the quotes) the comparison could have failed without this step." Actually ther is another way to solve it. In JavaScript you can use === which checks both value and type, while == only checks the value. Example: true == 'true'; //Returns true true === 'true'; //Returns false Hope it helps, Mikael SöderströmAnonymous
June 13, 2010
@ Mikael I agree - the double-equals should work (which is what I meant by " ... thought JavaScript would coerce the types..."), but in practice I found it to be unreliable, in particular for boolean comparisons. So I decided to do the conversion myself and just use the === operator instead. SimonAnonymous
August 02, 2010
The comment has been removedAnonymous
August 05, 2010
Thanks Luke! I'm afraid I don't think that's possible - at least not without some serious custom code in the form of model binders etc. You could look at MVC 3 to see if it gets easier too; I've not had a chance to dig into the detail yet. Stuart Leeks has some nice MVC 3 Validation intro posts; blogs.msdn.com/.../stuartleeks SimonAnonymous
March 09, 2011
The comment has been removedAnonymous
March 09, 2011
@ Sanjay; I'm not surprised, I've not tested it with many combinations of controls as it is just meant to be something to get you started. You probably need to look at the script that gets the current value of a control, and handle it differently for things like radios, checks, combos, etc. You may also get some data-type coersion errors. Fire up a JS debugger and you should spot these. It's also worth seeing the MVC3 version, although it'll likely suffer the same issues (these posts were never meant to be a finished product!); blogs.msdn.com/.../10125005.aspx Simon