Sunday 9 October 2011

MVC 3: Custom Validation

Every time you need validation in an application you end up with something that needs a specific validation rule.

There’s lots of blog post on how to implement custom validation using MVC 3 and jQuery validation (+unobtrusive). So why write another one? I’m going to share my thoughts on how it’s working and the steps for implementing it. Thus, this may be more of a note to my self…

Lets say we want to validate a Swedish personal identity number (it has a specific format and a control number suffix, it’s unimportant for this example, just mentioning something that's not built-in :-)). I like to start with the JavaScript part. Since user interaction response should be as quickly and close to the interaction as possible.

First register the JavaScript function (that will perform the actual validation) to the jquery validator plugin:
   1: $.validator.addMethod('personalid', function (value, element, params) {
   2:     // Check if the field is optional
   3:     if (this.optional(element))
   4:         return true;
   5:  
   6:     // Do some validation here (return true or false).    
   7:  
   8:     return true;
   9: });
Now we have a function called “personalid” that will be called on validation. The name "personalid" is important, it will be used later. Keep it in mind!
  • value: the value to validate.
  • element: the element being validated
  • params: we’ll get to this later.
Next we also register to the unobtrusive validation adapter. It will convert the HTML5 data-val-personalid to a format that jQuery validator can handle. We’ll get to that later.
1: $.validator.unobtrusive.adapters.addBool('personalid');
Here we have some different functions to choose from. Depending on which we use, it will affect the params parameter in the previous function registration, like this:
  • addBool(‘personalid’): (the one we use) – params will be true.
  • addSingleVal(‘personalid’, ‘someparam’): params will contain a provided value (I'll show a little about that later…).
  • addMinMax(‘personalid’, ‘minparam’, ‘maxparam’): params will have params.min and params.max attributes containing values by HTML5 data attributes.
  • add: full “control”. requires a more detailed example:
1: $.validator.unobtrusive.adapters.add("personalid", ["param1", "param2", "param3"],
   2:     function (options) {
   3:         options.rules['personalid'] = {
   4:             param1: options.params.param1,
   5:             param2: options.params.param2,
   6:             param3: options.params.param3
   7:         };
   8:         options.messages['personalid'] = options.message;
   9:     }
  10: );
Here params will contain params.param1, params.param2 and params.param3.

Back to our validation. We only use addBool so let's write some C# for server validation and also to generate the HTML5 data-attributes to the MVC view.
1: public sealed class PersonalIdentityNumberAttribute : ValidationAttribute, IClientValidatable
   2: {
   3:     private const string DefaultErrorMessage = "Invalid {0}";
   4:  
   5:     public string SomeParam { get; set; }
   6:  
   7:     public PersonalIdentityNumberAttribute()
   8:         : base(DefaultErrorMessage)
   9:     { }
  10:  
  11:     public override string FormatErrorMessage(string name)
  12:     {
  13:         // "name" is DisplayName of the property being validated.
  14:         // ErrorMessageString is set in constructor.
  15:         return string.Format(ErrorMessageString, name);
  16:     }
  17:  
  18:     protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  19:     {
  20:         if (value == null)
  21:             return ValidationResult.Success;
  22:  
  23:         // Do some validation here, return ValidationResult.Success or new ValidationResult() for error.
  24:  
  25:         return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
  26:     }
  27:  
  28:  
  29:     public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
  30:     {
  31:         ModelClientValidationRule clientValidationRule = new ModelClientValidationRule
  32:                                                              {
  33:                                                                  ErrorMessage = FormatErrorMessage(metadata.DisplayName),
  34:                                                                  ValidationType = "personalid"
  35:                                                              };
  36:         clientValidationRule.ValidationParameters.Add("someparam", SomeProperty);
  37:  
  38:         return new[] {clientValidationRule};
  39:  
  40:     }
  41: }

I think you can read the code yourself. Just some notes; SomeParam is totally unnecessary, I just want to show the principle. It will not even be parsed since we uses addBool, hence extra params will be ignored. But most important see lines 31-36, ValidationType is important. It corrolates to the name we registered in the jQuery validation above (addBool). I told you to remember this name ("personalid")! Also ValidationParameters.Add is important since it belongs to the name of addSingleVal above (but we don't use that one in this example). Or we can add more properties and use the add jQuery validation function.

Now we can use this attribute in our model:
   1: [Required(ErrorMessage = "Required")]
   2: [PersonalIdentityNumber(SomeParam = "foo")]
   3: [Display(Name = "Personal Identity Number")]
   4: public string PersonalIdentityNumber { get; set; }
Since we’ve implemented GetClientValidationRules this will be rendered as:
<input 
data-val="true" 
data-val-personalid="Invalid Personal Identity Number" 
data-val-personalid-someparam="foo"
data-val-required="Required"
id="PersonalIdentityNumber" name="PersonalIdentityNumber" type="text" value="" />

data-val-personalid comes from ValidationType (data-val-xxx) and has the value of the DisplayName. data-val-personalid-someparam comes from ValidationParameters.Add and has the value of SomeParam.

Now this can be used by the unobtrusive JavaScript we registered. And we have also created the server side validation.

If we had used the addSingleVal('personalid', 'someparam') instead, the params parameter would have been 'foo':
$.validator.addMethod('personalid', function (value, element, params) {
alert(params); // will alert: 'foo'
}

Or, if we had used add('personalid', ['param1', 'param2', 'param3']) instead (and had properties in the C# code, above, for them) the params parameter would have attributes like this:
data-val-personalid-param1="one"
data-val-personalid-param2="two"
data-val-personalid-param3="three"

$.validator.addMethod('personalid', function (value, element, params) {
alert(params.param1); // will alert: 'one'
alert(params.param2); // will alert: 'two'
alert(params.param3); // will alert: 'three'
}


Want to know how to really validate a swedish personal identity number? Read more here http://en.wikipedia.org/wiki/Personal_identity_number_(Sweden)

No comments:

Post a Comment