12.20.2009

MVC 2.0 Client validation exposed

There are many good articles about new validation model in MVC 2.0, for example an excellent article ASP.NET MVC 2 Custom Validation on Phil Haack's blog.
Yeah, data annotation and rules in model is cool. But I have found no info about how to add client validation rules from code, it can be useful in many cases. So, I go to Reflector and try to find some way to do this.
After a hour of browsing, I found that there is a class FormContext. Here is a prototype of this class:

    public class FormContext
    {
        public FormContext();

        public bool ClientValidationEnabled { get; set; }
        public string ClientValidationFunction { get; set; }
        public object ClientValidationState { get; set; }
        public IDictionary<string, FieldValidationMetadata> FieldValidators { get; }
        public string FormId { get; set; }
        public string ValidationSummaryId { get; set; }

        public string GetJsonValidationMetadata();
        public FieldValidationMetadata GetValidationMetadataForField(string fieldName);
        public FieldValidationMetadata GetValidationMetadataForField(string fieldName, bool createIfNotFound);
    }

This class available only between Html.BeginForm() and Html.EndForm() methods, in other words - only inside the form. You can access it from view, using inline code, as listed below:

<% ViewContext.FormContext %>

In the class signature there is an overloaded method GetValidationMetadataForField, it returns instance of the FieldValidationMetadata class for the specified field name of the form. FieldValidationMetadata contains ValidationRules property that we can change - add new rules. Cool! Here is a prototype of this class:

    public class FieldValidationMetadata
    {
        public FieldValidationMetadata();

        public string FieldName { get; set; }
        public bool ReplaceValidationMessageContents { get; set; }
        public string ValidationMessageId { get; set; }
        public ICollection<ModelClientValidationRule> ValidationRules { get; }
    }

As you can see from signature, property ValidationRules is a collection of ModelClientValidationRule class. It is a base class for all validation rules.
So, to say programmatically that field "price" is a required field, we must do the next:

<% using (Html.BeginForm()) { %>
// ............

<%
       FieldValidationMetadata metadata = ViewContext.FormContext.GetValidationMetadataForField("price", true);
       metadata.ValidationRules.Add(new ModelClientValidationRequiredRule("Please, specify the price."));
%>

// ............
<% } %>

Good, but not excellent. To do this with fluent syntax, and to emphasize that it is can be done only inside a form I had created extension method for MvcForm:

using System;
using System.Collections.ObjectModel;
using System.Web.Mvc;
using System.Web.Mvc.Html;

namespace HennadiyKurabko.Web.Mvc.Html.Extensions
{
    /// <summary>
    /// Represents support for adding client validation rules.
    /// </summary>
    public static class MvcFormExtensions
    {
        /// <summary>
        /// Adds the client validation rules to specified fields.
        /// </summary>
        /// <param name="form">The MVC form.</param>
        /// <param name="formContext">Context of the form.</param>
        /// <param name="validationRuleDescriptors">Descriptors of fields and related validation rules.</param>
        /// <returns>An <see cref="MvcForm"/> (for fluent syntax).</returns>
        /// <exception cref="System.ArgumentNullException"><paramref name="form"/>, <paramref name="formContext"/> or <paramref name="validationRuleDescriptors"/> is null.</exception>
        public static MvcForm AddClientValidationRules(this MvcForm form, FormContext formContext, ValidationRuleDescriptorCollection validationRuleDescriptors)
        {
            foreach (ValidationRuleDescriptor descriptor in validationRuleDescriptors)
            {
                if (descriptor.Rules == null || descriptor.Rules.Count == 0) continue;

                FieldValidationMetadata metadata = formContext.GetValidationMetadataForField(descriptor.FieldName, descriptor.CreateIfNotFound);

                foreach (ModelClientValidationRule rule in descriptor.Rules)
                    metadata.ValidationRules.Add(rule);
            }

            return form;
        }
    }

    /// <summary>
    /// Encapsulates the name of the field and related client validation rules.
    /// </summary>
    public class ValidationRuleDescriptor
    {
        #region Fields

        private string _fieldName;
        private ModelClientValidationRuleCollection _rules;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        public ValidationRuleDescriptor()
        {
            CreateIfNotFound = true;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        public ValidationRuleDescriptor(string fieldName)
            : this()
        {
            FieldName = fieldName;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="createIfNotFound">true to create a validation value if one is not found; otherwise, false.</param>
        public ValidationRuleDescriptor(string fieldName, bool createIfNotFound)
            : this(fieldName)
        {
            CreateIfNotFound = createIfNotFound;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="rules">Client validation rules.</param>
        public ValidationRuleDescriptor(string fieldName, ModelClientValidationRuleCollection rules)
            : this(fieldName)
        {
            Rules = rules;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ValidationRuleDescriptor"/> class.
        /// </summary>
        /// <param name="fieldName">Name of the associated field.</param>
        /// <param name="createIfNotFound">true to create a validation value if one is not found; otherwise, false.</param>
        /// <param name="rules">Client validation rules.</param>
        public ValidationRuleDescriptor(string fieldName, bool createIfNotFound, ModelClientValidationRuleCollection rules)
            : this(fieldName, createIfNotFound)
        {
            Rules = rules;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a name of the field.
        /// </summary>
        public string FieldName
        {
            get { return _fieldName; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("FieldName");
                if (string.IsNullOrEmpty(value))
                    throw new ArgumentException("Cannot be an empty string.", "FieldName");

                _fieldName = value;
            }
        }

        /// <summary>
        /// Gets or sets a value that indicates what to do if the validation value is not found.
        /// </summary>
        public bool CreateIfNotFound { get; set; }

        /// <summary>
        /// Gets or sets a collection of client validation rules.
        /// </summary>
        public ModelClientValidationRuleCollection Rules
        {
            get { return _rules; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("Rules");

                _rules = value;
            }
        }

        #endregion
    }

    /// <summary>
    /// A collection of <see cref="ValidationRuleDescriptor"/> instances representing fields and related validation rules.
    /// </summary>
    [Serializable]
    public class ValidationRuleDescriptorCollection : Collection<ValidationRuleDescriptor>
    {
    }

    /// <summary>
    /// A collection of <see cref="ModelClientValidationRule"/> instances representing set of client validation rules.
    /// </summary>
    [Serializable]
    public class ModelClientValidationRuleCollection : Collection<ModelClientValidationRule>
    {
    }
}

So, to add validation rule you can write next code:

<% using (Html.BeginForm().AddClientValidationRules(ViewContext.FormContext, new ValidationRuleDescriptorCollection()
   {
       new ValidationRuleDescriptor("price", true, new ModelClientValidationRuleCollection() {
           new ModelClientValidationRequiredRule("It is a custom rule."),
           new ModelClientValidationRangeRule("Value must be greater than zero, and less than or equal to 10.", 0, 10)
       })
   }))
   { %>

// ...............
<% } %>

An example you can download here: ManualClientValidation.zip

Good luck, and happy coding!

Shout it

kick it on DotNetKicks.com

2 comments:

  1. Great post, this really helped me with implementing some client side validation with validation parameters which change at runtime.

    ReplyDelete