Client and Server Side Validation in MVC 3

In this post I would like to examine how to create integrated client and server side validation in MVC 3.  The example is a bit contrived, but here is the just of it.  I have a person class and I want to make sure that email address does not contain first or last name and also cannot be longer than 50 characters.  Here is why I picked this example.  I want to be able to pass in value (maximum length) and also create parallel client / server side validation that enforces this rule.  This rule will dynamically get the values from last and first name fields and compare it to email field.  OK, now that the goal is stated, time to work on implementation.

First, let’s work on server side.  We can never trust the client, so server must enforce all client rules again.  Server side coding is very easy and can be easily accomplished via an attribute.  There is already a number of attribute, such as Required that we can use, but I want to create a custom rule in this case.  So, I will create brand new attribute, and take advantage of existing MVC functionality by inheriting from ValidationAttribute.  To make it more versatile, I am going to rely on a custom interface:

    public interface IPersonWithEmail
    {
       
string FirstName { get; set; }
       
string LastName { get; set
; }
       
string Email { get; set
; }
    }

 

Now, the attribute itself:

  [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
 
public class AdvancedEmailValidationAttribute : ValidationAttribute

  {
   
public
AdvancedEmailValidationAttribute()
      :
this
(0)
    {
    }
 
   
public AdvancedEmailValidationAttribute(int
maximumLength)
    {
      MaximumLength = maximumLength == 0 ?
        50 : maximumLength;
      ErrorMessage =
string.Format("Email cannot contain first or last name and cannot be longer than {0} characters."
, MaximumLength);
    }
   
public int MaximumLength { get; private set
; }
 
   
protected override ValidationResult IsValid(object value, ValidationContext
validationContext)
    {
     
var objectToValidate = validationContext.ObjectInstance as IPersonWithEmail
;
     
ValidationResult returnValue = ValidationResult
.Success;
 
     
if (objectToValidate != null
)
      {
       
if (value != null
)
        {
         
if
(
            value.ToString().ToUpper().Contains(objectToValidate.LastName.ToUpper()) ||
            value.ToString().ToUpper().Contains(objectToValidate.LastName.ToUpper()) || 
            value.ToString().Length > MaximumLength)
          {
            returnValue =
new ValidationResult
(ErrorMessage);
          }
        }
      }
     
else

      {
        returnValue =
new ValidationResult("You must implement IPersonWithEmail on your object to use this rule");
      }
     
return
returnValue;
    }
 
  }

 

As you can see above, I am have a custom property called MaximumLength that I am populating via constructor.  This promotes attribute reuse.  Then, I am overriding IsValid method.  This method simply checks the rule.  If rule succeeds, it returns ValidationResult.Success.  Otherwise it returns instance of ValidationResult with a specific error message.  That is all.  To use the attribute, I simply decorate the email field with this attribute as below:

    [AdvancedEmailValidation()]
   
public string Email { get; set
; }

 

Now, if I run the sample, I will see this in action.  Because I do not have client rules yet, I will see the postback, but when my screen comes back, the error will be shown.

To make this functionality more interactive, I am now going to add client side validation.  TO start with, I will extend my attribute with client side validation using IClientValidatable interface.  Here is what it looks like:

  [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
 
public class AdvancedEmailValidationAttribute : ValidationAttribute, IClientValidatable

  {
   
public AdvancedEmailValidationAttribute()
      :
this
(0)
    {
    }
 
   
public AdvancedEmailValidationAttribute(int
maximumLength)
    {
      MaximumLength = maximumLength == 0 ?
        50 : maximumLength;
      ErrorMessage =
string.Format("Email cannot contain first or last name and cannot be longer than {0} characters."
, MaximumLength);
    }
   
public int MaximumLength { get; private set
; }
 
   
protected override ValidationResult IsValid(object value, ValidationContext
validationContext)
    {
     
var objectToValidate = validationContext.ObjectInstance as IPersonWithEmail
;
     
ValidationResult returnValue = ValidationResult
.Success;
 
     
if (objectToValidate != null
)
      {
       
if (value != null
)
        {
         
if
(
            value.ToString().ToUpper().Contains(objectToValidate.LastName.ToUpper()) ||
            value.ToString().ToUpper().Contains(objectToValidate.LastName.ToUpper()) || 
            value.ToString().Length > MaximumLength)
          {
            returnValue =
new ValidationResult
(ErrorMessage);
          }
        }
      }
     
else

      {
        returnValue =
new ValidationResult("You must implement IPersonWithEmail on your object to use this rule");
      }
     
return
returnValue;
    }
 
   
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext
context)
    {
     
var rule = new ModelClientValidationRule

      {
        ErrorMessage = ErrorMessage,
        ValidationType =
"advancedemail"
      };
      rule.ValidationParameters.Add(
"maxlength", MaximumLength);
      rule.ValidationParameters.Add(
"firstname", "FirstName"
);
      rule.ValidationParameters.Add(
"lastname", "LastName"
);
     
yield return
rule;
 
    }
  }

 

This interface only has one member – method called GetClientValidaitonRules.  In my case I am only returning one rule.  I can create a custom rule by inheriting from ModelClientValidationRule, but my use case is simple, so I am not doing it.  I am also adding validation parameters (all strings) that I will need on client side.  It is obviously maximum length, and first and last name property names.  I will use all three at the client to compare email to first and last name as well as maximum length.  The next step in the process is to use jQuery  validation and write methods that inject validation as well as enforce it.

Here is my script (I am including entire partial view for clarity)

@if (false)

 
<reference src="../../Scripts/jquery-1.5.1-vsdoc.js" type="text/javascript" />

 
<reference src="../../Scripts/jquery.validate.js" type="text/javascript" />
 
<reference src="../../Scripts/jquery.validate.unobtrusive.js" type="text/javascript" />
}
@model MvcEFCodeFirst.Data.
Attendee
@
using (Html.BeginForm(new { Id = "AttendeeForm" }))
{
  @Html.ValidationSummary(
true
)
 
<fieldset>

   
<legend>Attendee</legend>
   
<div class="editor-label">
      @Html.LabelFor(model => model.FirstName)
   
</div>
   
<div class="editor-field">
      @Html.EditorFor(model => model.FirstName)
      @Html.ValidationMessageFor(model => model.FirstName)
   
</div>
   
<div class="editor-label">
      @Html.LabelFor(model => model.LastName)
   
</div>
   
<div class="editor-field">
      @Html.EditorFor(model => model.LastName)
      @Html.ValidationMessageFor(model => model.LastName)
   
</div>
   
<div class="editor-label">
      @Html.LabelFor(model => model.Email)
   
</div>
   
<div class="editor-field">
      @Html.EditorFor(model => model.Email)
      @Html.ValidationMessageFor(model => model.Email)
   
</div>
   
<div class="editor-label">
      @Html.LabelFor(model => model.Notes)
   
</div>
   
<div class="editor-field">
      @Html.EditorFor(model => model.Notes)
      @Html.ValidationMessageFor(model => model.Notes)
   
</div>
    @
if (Model.AttendeeID > 0)
    { 
      @Html.HiddenFor(model => model.AttendeeID)
    }
 
</fieldset>

}
<script type="text/javascript">
 
 
  (
function ($) {
 
    $.validator.addMethod(
‘advancedemail’, function
(value, element, param) {
     
if (!value) return false
;
     
var lastName = $(‘#’
+ param.lastname).val();
     
var firstName = $(‘#’
+ param.firstname).val();
     
var
maxlength = param.maxlength;
     
if
(value.toString().toLowerCase().indexOf(lastName.toString().toLowerCase()) >= 0 ||
        value.toString().toLowerCase().indexOf(firstName.toString().toLowerCase()) >= 0 ||
          value.toString().length > maxlength) {
       
return false
;
      }
     
return true
;
    });
 
 
    $.validator.unobtrusive.adapters.add(
     
‘advancedemail’
,
      [
‘maxlength’, ‘firstname’, ‘lastname’
],
     
function
(options) {
 
       
var
params = {
          maxlength: options.params.maxlength,
          firstname: options.params.firstname,
          lastname: options.params.lastname
        };
        
        options.rules[
‘advancedemail’
] = params;
       
if
(options.message) {
          options.messages[
‘advancedemail’
] = options.message;
        }
 
      });
  } (jQuery));

</script
>

 

As you can see from above, my code to inject the rule consists of two components – function that performs validation (follows validator.addMethod call) and function that injects it into validation infrastructure (validator.unobtrusive.adapters.add call).  First, let me examine the second function that does injection.  I am adding new unobtrusive adapater with the name of advancedemail.  This matches what I put into my attribute – this is how the attribute and client side work together.  I am pumping the parameters into function that will return validation method options as a class called “params”, as well as message.

The actual validation function is interesting as well.  I am getting the values from the form by using jQuery selector for the form input control name that is actually an option passed in by my attribute.  Then I am actaully enforcing the rule by doing basic string manipulation.  ALl I need to do is return true or false from the actual validation function, where false means the rule failed.  I am injecting the entire set of functionality into partial view by including a script tag that executes right away.  If you view source of this page, you will see how my attribute data got injected into the view:

    <div class="editor-label">

      <label for="Email">Email</label>

    </div>

    <div class="editor-field">

      <input class="text-box single-line" data-val="true" data-val-advancedemail="Email cannot contain first or last name and cannot be longer than 50 characters." data-val-advancedemail-firstname="FirstName" data-val-advancedemail-lastname="LastName" data-val-advancedemail-maxlength="50" data-val-length="The field Email must be a string with a maximum length of 250." data-val-length-max="250" data-val-regex="Email is not in a correct format" data-val-regex-pattern="^[A-Za-z0-9](([_.-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([.-]?[a-zA-Z0-9]+)*).([A-Za-z]{2,})$" data-val-remote="Email address is duplicate" data-val-remote-additionalfields="*.Email,*.AttendeeID" data-val-remote-url="/MvcEFCodeFirst/Attendee/DuplicateEmail" data-val-required="Email is required" id="Email" name="Email" type="text" value="" />

      <span class="field-validation-valid" data-valmsg-for="Email" data-valmsg-replace="true"></span>

    </div>

I know, a little hard to read, but you can certainly see the result of my attribute by looking at the data that starts with data-val-advancedemail.

One thing to note is that my validator name is all lower case, and this is because jQuery validation may generate the error – method name must be in lower case without digits.

To summarize, client and server side validation should both be covered when it comes to rules.  Server side can be easily enforced with ValidationAttribute.  Client side validation required three moving parts – IClientValidatable on the server side, function that performs validation at the client side and code that creates unobtrusive validation adapter that points to that function.

You can download entire sample here.

One Comment

  1. Hi ,

    Thanks for the great post, I have implimented the same. but I am facing an issue i.e. the validation error messages arent shown on client side validation. The scripts are firing fine (confirmed with alerts) and other built in validators work good on client side.

    any thoughts please ?

    complete details are here ( http://stackoverflow.com/questions/8634986/custom-model-validation-using-data-annotations-mvc3-error-messages-not-shown-o )

    Appreciate your kind help,

    Thanks

Leave a Reply

Your email address will not be published. Required fields are marked *