Wednesday, December 29, 2010

ASP.NET MVC 3 Custom Validation

Data annotations Rock!  Using data annotations on your model classes make data validation (client and server) trivial.  Just add the data annotation to you model class, wire up an edit/create view to your controller via the 'Add View' wizard, and you have client- and server-side validation with very little effort.

A little disclaimer here - the implementation below it not MVC 3 specific; however, I developed the solution using MVC 3, tested using MVC 3, and it works using MVC 3.  I'm pretty sure it will work as-is using MVC 2.

What if you need validation that is not provided via data annotations out-of-box?  You create your own.  That's what this post is all about.

Creating your own validators that support client- and server-side validation is essentially a four-step process:
  1. Create a custom attribute that extends the ValidationAttribute, or better yet, you extend one of the existing data annotations.
  2. Create a custom validator that extends the DataAnnotationsModelValidator, where T is the type of custom attribute you created in step one.
  3. Create a client-side script to handle the client-side validation.
  4. Register the attribute/validator classes in your app's bootstrapper or Global.asax.
Side note: It seems that every time I have a good idea for a post, Phil Haack has beat me to the punch... and this pattern is also true in this scenario :-) For a similar article on the same subject, please see Phil's ASP.NET MVC 2 Custom Validation post.

This post is very code centric; therefore, if the concepts are unfamiliar or a brushing up is necessary, please take a look at Phil's post.  Also, Brad Wilson has a few different blog posts/series on Data Annotations and ModelMetadata.  Brad and Phil's knowledge of the ins and outs of anything ASP.NET MVC (and arguably C#) scares me...

The custom attribute and validator that I'm going to create is an Email validator.  There are plenty examples on the net, including here, here, etc...  What these email validator solutions don't provide is client-side validation.  I will show you how.  So, lets start with step one:

1. Create a Custom Attribute
Below are two solutions.  They both result in the same functionality.  The first solution is a full implementation that extends the ValidationAttribute class, while the second solution extends the RegularExpressionAttribute.

EmailAttribute Extending Validation Attribute
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.RegularExpressions;

namespace Validation
{
    /// <summary>
    /// Email Attribute class used for email validation. Used similar to the System.ComponentModel.DataAnnotations.RegularExpressionAttribute.
    /// </summary>
    public class EmailAttribute : ValidationAttribute
    {
        #region Properties

        /// <summary>
        /// Gets or sets the Regular expression.
        /// </summary>
        /// <value>The regex.</value>
        private Regex Regex { get; set; }

        /// <summary>
        /// Gets the pattern used for email validation.
        /// </summary>
        /// <value>The pattern used for email validation.</value>
        /// <remarks>
        /// Regular Expression Source - Comparing E-mail Address Validating Regular Expressions
        /// <see cref="http://fightingforalostcause.net/misc/2006/compare-email-regex.php"/>
        /// </remarks>
        public string Pattern
        {
            get
            {
                return
                    @"^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-zA-Z0-9]{1}[a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]{1})|[a-zA-Z])\.)+[a-zA-Z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$";
            }
        }

        #endregion Properties

        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailAttribute"/> class.
        /// </summary>
        public EmailAttribute()
        {
            this.Regex = new Regex(this.Pattern);
        }

        #endregion Ctors

        /// <summary>
        /// Determines whether the specified value of the object is valid.
        /// </summary>
        /// <param name="value">The value of the object to validate.</param>
        /// <returns>
        /// true if the specified value is valid; otherwise, false.
        /// </returns>
        public override bool IsValid(object value)
        {
            // convert the value to a string
            var stringValue = Convert.ToString(value, CultureInfo.CurrentCulture);

            // automatically pass if value is null or empty. RequiredAttribute should be used to assert an empty value.
            if (string.IsNullOrWhiteSpace(stringValue)) return true;

            var m = Regex.Match(stringValue);

            // looking for an exact match, not just a search hit.
            return (m.Success && (m.Index == 0) && (m.Length == stringValue.Length));
        }
    }
}

EmailAttribute Extending RegularExpressionAttribute
using System.ComponentModel.DataAnnotations;

namespace Validation
{
    /// <summary>
    /// Email Attribute class used for email validation. Used similar to the System.ComponentModel.DataAnnotations.RegularExpressionAttribute.
    /// </summary>
    public class EmailAttribute : RegularExpressionAttribute
    {
        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailAttribute"/> class.
        /// </summary>
        public EmailAttribute()
            : base(
                @"^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-zA-Z0-9]{1}[a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]{1})|[a-zA-Z])\.)+[a-zA-Z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$"
                )
        {
        }

        #endregion Ctors
    }
}
ddd
Again, whatever implementation you choose is up to you.  Both implementations have the same resulting functionality - Email validation via data annotations.

2. Create a custom validator that extends the DataAnnotationsModelValidator
This class - the validator class - is the class that provides metadata to enable client-side validation.  This class essentially transfers settings, in our case the ErrorMessage and Pattern properties of the EmailAttribute class, from the attribute decorations on your model object to the client-side consumer.  The client-side consumer is what provides the client-side validation.

using System.Collections.Generic;
using System.Web.Mvc;
using Chinook.Framework.Validation;

namespace Chinook.Web.Core.Validation
{
    /// <summary>
    /// Provides a model validator for the EmailAttribute annotation.
    /// </summary>
    public class EmailValidator : DataAnnotationsModelValidator<EmailAttribute>
    {
        #region Fields

        private readonly string _errorMessage;
        private readonly string _pattern;

        #endregion Fields

        #region Ctors

        /// <summary>
        /// Initializes a new instance of the <see cref="EmailValidator"/> class.
        /// </summary>
        /// <param name="metadata">The metadata.</param>
        /// <param name="context">The context.</param>
        /// <param name="attribute">The attribute.</param>
        public EmailValidator(ModelMetadata metadata, ControllerContext context, EmailAttribute attribute)
            : base(metadata, context, attribute)
        {
            this._errorMessage = attribute.ErrorMessage;
            this._pattern = attribute.Pattern;
        }

        #endregion Ctors

        #region Methods

        /// <summary>
        /// Retrieves a collection of client validation rules.
        /// </summary>
        /// <returns>A collection of client validation rules.</returns>
        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
        {
            var rule = new ModelClientValidationRegexRule(this._errorMessage, this._pattern);
            return new[] {rule};
        }

        #endregion Methods
    }
}
So now that we have the attribute and validator classes created, we need to...

3. Create a client-side script to handle the client-side validation
I feel like I'm cheating on you here.  Out scenario - email validation - and it's implementation do not require us to write client-side script.  I know, I know... if you feel robbed, you can create your own client-side script, and register is with the jQuery Validate plugin.  I'll try and create a future post will another validator that requires writing client-side script and jQuery Validate registration, but for now we going to enjoy the luxuries of extending and using existing functionality.  But, HOW do we get away with using existing client-side script?

Take a look at the following code snippet from step two:
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
    var rule = new ModelClientValidationRegexRule(this._errorMessage, this._pattern);
    return new[] {rule};
}

This method 'retrieves' a collection of client validation rules.'  And since we are essentially using a RegularExpressionAttribute class (via extension), we can use the client validation rules used by the RegularExpressionAttribute's Model Validator class via the ModelClientValidationRegexRule passing our Error Message and Regular Expression pattern.

This bring us to our last step...

4. Register the attribute/validator classes in your app's bootstrapper or Global.asax
To make all this sweetness happen, we need to register the attribute and validator classes when the app domain kicks off its like cycle.  In your Global.asax's Application_Start method, register the attribue and validator classes using the following code snippet:
// register custom model validators
DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAttribute), typeof(EmailValidator));
To use the attribute and validator classes use just created, you need to decorate you model/POCO classes with data annotations, including your EmailAttribute class.  The following shows how the EmailAttribute is used (along with other data annotations) to decorated a property in your model class:
[Display(Name = "Email")]
[Required(ErrorMessage = "Email is Required.")]
[Email(ErrorMessage = "Not a valid Email Address.")]
[StringLength(60, ErrorMessage = "Email must be under 60 characters.")] 
public string Email { get; set; }

Once you create strongly typed Edit and/or Create Views using your models, include the script libraries, and run the application, you will see the fruits of your labor and get client-side validation.  The screenshot below is an example.

Anyway, I hope you got a little something out of this post.

Thanks for reading...

10 comments:

mattyboy said...

Nice post man, thanks.

tdryan said...

Thanks Matt...

jmcilhinney said...

Excellent. Exactly what I was looking for, down to the fact that it validates email addresses.

scr said...

Rock. Just want I was needing. Great post.

Anders said...

Thanks a lot. Really helped me out.

tdryan said...

Pleasure Anders - happy it helped. Thanks for the comment...

Andrew Bevan said...

Thanks helped allot...
Was battling with the ClientSide wireup.

Andrew Bevan said...

Thanks helped allot...
Was battling with the ClientSide wireup.

Phil Hale said...

Useful post thanks. Interestingly it looks like you can do without the EmailValidator class as suggested in this Stack Overflow post:

http://stackoverflow.com/a/4268498/299048

DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(EmailAttribute), typeof(RegularExpressionAttributeAdapter));

Jamie Meyer said...

This is AWESOME!!! I have been fighting with this all day, and then in 20 minutes had it working with your code.

This is the first coherent complete example that I have found anywhere.

Thanks!