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:
- Create a custom attribute that extends the ValidationAttribute, or better yet, you extend one of the existing data annotations.
- Create a custom validator that extends the DataAnnotationsModelValidator
, where T is the type of custom attribute you created in step one. - Create a client-side script to handle the client-side validation.
- Register the attribute/validator classes in your app's bootstrapper or Global.asax.
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...
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)); } } }
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 } }
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 } }
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));
[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; }
Anyway, I hope you got a little something out of this post.
Thanks for reading...