Validating Dependent Fields in ASP.NET MVC

In this post I wanted to describe a solution to a specific problem I recently encountered.  The problem is as following.  I have a class with a set of dependent properties, such as start and end date or minimum / maximum numbers.  I want to implement both client and server side validation in an MVC application using attributes.  I really like how attributes work in MVC application that is why I picked that specific solution.  Since I want good interactive user experience, I want to make sure the same validation works in JavaScript.  I am going to build my validation by plugging into jQuery validation framework.

I am now going to assume the following:

  • You created new MVC 3 project using Razor
  • I added Entity Framework to the project
  • I updated all packages to the latest version

Cool, now that we established ground work, le’s describe the solution.  Let’s start with a Product class.

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcValidation.Data
{
    public class Product
    {
        public int Id { get; set; }

        [StringLength(30)]
        [Required]
        public string Name { get; set; }

        [Display(Name = "Start Date")]
        [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
        [CompareDates("EndDate", ErrorMessage = "Start date cannot be before end date")]
        public DateTime? BeginDate { get; set; }

        [Display(Name = "End Date")]
        [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
        [CompareDates("BeginDate", ErrorMessage = "Start date cannot be before end date")]
        public DateTime? EndDate { get; set; }

        [Display(Name = "Min. Quantity")]
        [CompareNumbers("EndQuantity", ErrorMessage = "Min cannot be more than max")]
        public int BeginQuantity { get; set; }

        [Display(Name = "Max. Quantity")]
        [CompareNumbers("BeginQuantity", ErrorMessage = "Min cannot be more than max")]
        public int EndQuantity { get; set; }
    }
}

As you can see from above, I am going to create a couple of attributes, one to compare number, the other to compare dates.  I am going to cover one in details, and the other one is very similar of course.  To create a validation attribute, such as CompareNumbers, I am going to extend a base class, ValidationAttribute.  Then, I am going to implement an IClientValidatable interface.  This intercface only needs one method, the one that returns validation rules collection. In my case, I only need one rule.

First, server side.  I am going to override IsValid method.and implement simple logic.  To do that, I need to have access to two things, current value which is passed into this method, and the other property value that I need to compare this value to.  To simplify the setup my logic assumes that if property name contains word “begin”, it would be a start (smaller value).  Otherwise, I am going to assume that I am looking at the larger value of the two.

/// <summary>
        /// Validates the specified value with respect to the current validation attribute.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var result = ValidationResult.Success;
            var otherValue = validationContext.ObjectType.GetProperty(OtherPropertyName).GetValue(validationContext.ObjectInstance, null);
            if (value != null)
            {
                decimal currentDecimalValue;
                if (decimal.TryParse(value.ToString(), out currentDecimalValue))
                {

                    if (otherValue != null)
                    {
                        decimal otherDecimalValue;
                        if (decimal.TryParse(otherValue.ToString(), out otherDecimalValue))
                        {
                            if (!OtherPropertyName.ToLower().Contains("begin"))
                            {
                                if (currentDecimalValue > otherDecimalValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            else
                            {
                                if (currentDecimalValue < otherDecimalValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            if (currentDecimalValue == otherDecimalValue && !AllowEquality)
                            {
                                result = new ValidationResult(ErrorMessage);
                            }
                        }
                    }
                }
            }
            return result;
        }

If you look at the logic, you can see that I am doing null check first, then converting current value to decimal.  if you want to extend this validation to double type, you can easily do that.  Also, I have a check for equal values, and in this case I am going to use explicit property value of the attribute.  let me show you the entire class now:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace MvcValidation.Data
{
    /// <summary>
    /// Compares two dates to each other, ensuring that one is larger than the other
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CompareNumbersAttribute : ValidationAttribute, IClientValidatable
    {

        /// <summary>
        /// Initializes a new instance of the <see cref="CompareNumbersAttribute"/> class.
        /// </summary>
        /// <param name="otherPropertyName">Name of the compare to date property.</param>
        /// <param name="allowEquality">if set to <c>true</c> equal dates are allowed.</param>
        public CompareNumbersAttribute(string otherPropertyName, bool allowEquality = true)
        {
            AllowEquality = allowEquality;
            OtherPropertyName = otherPropertyName;
        }

        #region Properties

        /// <summary>
        /// Gets the name of the  property to compare to
        /// </summary>
        public string OtherPropertyName { get; private set; }

        /// <summary>
        /// Gets a value indicating whether dates could be the same
        /// </summary>
        public bool AllowEquality { get; private set; }


        #endregion

        /// <summary>
        /// Validates the specified value with respect to the current validation attribute.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var result = ValidationResult.Success;
            var otherValue = validationContext.ObjectType.GetProperty(OtherPropertyName).GetValue(validationContext.ObjectInstance, null);
            if (value != null)
            {
                decimal currentDecimalValue;
                if (decimal.TryParse(value.ToString(), out currentDecimalValue))
                {

                    if (otherValue != null)
                    {
                        decimal otherDecimalValue;
                        if (decimal.TryParse(otherValue.ToString(), out otherDecimalValue))
                        {
                            if (!OtherPropertyName.ToLower().Contains("begin"))
                            {
                                if (currentDecimalValue > otherDecimalValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            else
                            {
                                if (currentDecimalValue < otherDecimalValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            if (currentDecimalValue == otherDecimalValue && !AllowEquality)
                            {
                                result = new ValidationResult(ErrorMessage);
                            }
                        }
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// When implemented in a class, returns client validation rules for that class.
        /// </summary>
        /// <param name="metadata">The model metadata.</param>
        /// <param name="context">The controller context.</param>
        /// <returns>
        /// The client validation rules for this validator.
        /// </returns>
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ErrorMessage = ErrorMessage,
                ValidationType = "comparenumbers"
            };
            rule.ValidationParameters["otherpropertyname"] = OtherPropertyName;
            rule.ValidationParameters["allowequality"] = AllowEquality ? "true" : "";
            yield return rule; 
        }
    }
}

As you can see, I have two properties that I am setting from the constructor – the other property name and a flag that allows values to be equal, which is my default.  If you take a look at the last method, you can see that I am injecting those two properties’ values into the HTML of the page via jQuery validation and unobtrusive validation frameworks.  Unobtrusive validation works off data-val-* attributes that MVC automatically injects for me into my HTML and jQuery parses those, establishing client side validation rules.

Here is how I am going to approach client side.  I am going to write a JavaScript function, similar to IsValid method you saw above.  To inject this method into jQuery validation, I just need to call two methods in jQuery: I need to add an adapter via adapters.add method that would adept data-val-* attribute values and will map them to parameters passed into my function, which in turn is added via addMethod call.  Once that is done, I am going to subscribe to blur (lost focus) method of the “other” control and run its validation at that point.  So, if I got it all right, as soon I change a value of Begin*** text box, then tab out, both that textbox and End*** textbox’s value will be validated by my custom method.

So, here is how I am going to add an adapter:

    $.validator.unobtrusive.adapters.add('comparenumbers', ['otherpropertyname', 'allowequality'],
        function (options) {
            options.rules['comparenumbers'] = options.params;
            if (options.message) {
                options.messages['comparenumbers'] = options.message;
            }
        }
    );

As you can see, I just need to le jQuery know about my property names of the original validation attribute.  All values and names must be in lower case though, or you will get errors from jQuery.  These values also include error message that I originally set on the attribute constructor called from my Product class (see class definition above.).

Next step is to add my validation function:

$.validator.addMethod('comparenumbers', function (value, element, params) {
    var otherFieldValue = $('input[name="' + params.otherpropertyname + '"]').val();
    if (otherFieldValue && value) {
        var currentValue = parseFloat(value);
        var otherValue = parseFloat(otherFieldValue);
        if ($(element).attr('name').toLowerCase().indexOf('begin') >= 0) {
            if (params.allowequality) {
                if (currentValue > otherValue) {
                    return false;
                }
            } else {
                if (currentValue >= otherValue) {
                    return false;
                }
            }
        } else {
            if (params.allowequality) {
                if (currentValue < otherValue) {
                    return false;
                };
            } else {
                if (currentValue <= otherValue) {
                    return false;
                };
            }
        }
    }
    customValidation.addDependatControlValidaitonHandler(element, params.otherpropertyname);
    return true;
}, '');

Code is pretty simple as you can see.  Again, I am relying on property names (control names to be precise), but those do match because this is how MVC engine creates controls corresponding to properties.  I am also doing null checks and calling parseFloat methods.  I also check equality property value.   I rely on the fact that in JavaScript non empty string check returns True in my if statement.  The last line of code is interesting.  I have a helper class that is responsible for wiring up dependent controls via addDependatControlValidaitonHandler method.  let’s take a look at this helper class.

window.customValidation = window.customValidation ||
    {
        relatedControlValidationCalled: function (event) {
            if (!customValidation.activeValidator) {
                customValidation.formValidator = $(event.data.source).closest('form').data('validator');
            }
            customValidation.formValidator.element($(event.data.target));
        },
        relatedControlCollection: [],
        formValidator: undefined,
        addDependatControlValidaitonHandler: function (element, dependentPropertyName) {
            var id = $(element).attr('id');
            if ($.inArray(id, customValidation.relatedControlCollection) < 0) {
                customValidation.relatedControlCollection.push(id);
                $(element).on(
                    'blur',
                    { source: $(element), target: $('#' + dependentPropertyName) },
                    customValidation.relatedControlValidationCalled);
            }
        }
    };

My method addDependatControlValidaitonHandler checks to see if customValidation already tracks the dependent control, and if not, adds it to wired controls collection (relatedControlsCollection property).  First time validation is called, I am also grabbing validator object from the form to use with calls.  This is an optimization trick.  In the addDependatControlValidaitonHandler I also attach an event handler to the ‘blur’ (lost focus) event of the related control. I am using a few utility method from jQuery to accomplish that, such as on and inArray.

As far as dates validation goes, the only difference is parsing.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace MvcValidation.Data
{
    /// <summary>
    /// Compares two dates to each other, ensuring that one is larger than the other
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class CompareDatesAttribute : ValidationAttribute, IClientValidatable
    {

        /// <summary>
        /// Initializes a new instance of the <see cref="CompareDatesAttribute"/> class.
        /// </summary>
        /// <param name="otherPropertyName">Name of the compare to date property.</param>
        /// <param name="allowEquality">if set to <c>true</c> equal dates are allowed.</param>
        public CompareDatesAttribute(string otherPropertyName, bool allowEquality = true)
        {
            AllowEquality = allowEquality;
            OtherPropertyName = otherPropertyName;
        }

        #region Properties

        /// <summary>
        /// Gets the name of the  property to compare to
        /// </summary>
        public string OtherPropertyName { get; private set; }

        /// <summary>
        /// Gets a value indicating whether dates could be the same
        /// </summary>
        public bool AllowEquality { get; private set; }


        #endregion

        /// <summary>
        /// Validates the specified value with respect to the current validation attribute.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var result = ValidationResult.Success;
            var otherValue = validationContext.ObjectType.GetProperty(OtherPropertyName)
                .GetValue(validationContext.ObjectInstance, null);
            if (value != null)
            {
                if (value is DateTime)
                {

                    if (otherValue != null)
                    {
                        if (otherValue is DateTime)
                        {
                            if (!OtherPropertyName.ToLower().Contains("begin"))
                            {
                                if ((DateTime)value > (DateTime)otherValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            else
                            {
                                if ((DateTime)value < (DateTime)otherValue)
                                {
                                    result = new ValidationResult(ErrorMessage);
                                }
                            }
                            if ((DateTime)value == (DateTime)otherValue && !AllowEquality)
                            {
                                result = new ValidationResult(ErrorMessage);
                            }
                        }
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// When implemented in a class, returns client validation rules for that class.
        /// </summary>
        /// <param name="metadata">The model metadata.</param>
        /// <param name="context">The controller context.</param>
        /// <returns>
        /// The client validation rules for this validator.
        /// </returns>
        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ErrorMessage = ErrorMessage,
                ValidationType = "comparedates"
            };
            rule.ValidationParameters["otherpropertyname"] = OtherPropertyName;
            rule.ValidationParameters["allowequality"] = AllowEquality ? "true" : "";
            yield return rule; 
        }
    }
}
$.validator.unobtrusive.adapters.add('comparedates', ['otherpropertyname', 'allowequality'],
        function (options) {
            options.rules['comparedates'] = options.params;
            if (options.message) {
                options.messages['comparedates'] = options.message;
            }
        }
    );

$.validator.addMethod('comparedates', function (value, element, params) {
    var otherFieldValue = $('input[name="' + params.otherpropertyname + '"]').val();
    if (otherFieldValue && value) {
        var currentValue = Date.parse(value);
        var otherValue = Date.parse(otherFieldValue);
        if ($(element).attr('name').toLowerCase().indexOf('begin') >= 0) {
            if (params.allowequality) {
                if (currentValue > otherValue) {
                    return false;
                }
            } else {
                if (currentValue >= otherValue) {
                    return false;
                }
            }
        } else {
            if (params.allowequality) {
                if (currentValue < otherValue) {
                    return false;
                }
            } else {
                if (currentValue <= otherValue) {
                    return false;
                }
            }
        }
    }
    customValidation.addDependatControlValidaitonHandler(element, params.otherpropertyname);
    return true;
}, '');

That is all there is to it.  My cshtml page looks the same as the one without this validation, which is cool.  I like clean and simple HTML.

@model MvcValidation.Data.Product

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>



@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Product</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.BeginDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.BeginDate)
            @Html.ValidationMessageFor(model => model.BeginDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.EndDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.EndDate)
            @Html.ValidationMessageFor(model => model.EndDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.BeginQuantity)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.BeginQuantity)
            @Html.ValidationMessageFor(model => model.BeginQuantity)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.EndQuantity)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.EndQuantity)
            @Html.ValidationMessageFor(model => model.EndQuantity)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>
<script type="text/javascript">
    $(function () {
        $('#BeginDate').datepicker();
        $('#EndDate').datepicker();
    });
</script>

One nice thing I also do is to use jQuery UI framework to turn my text boxes into date pickers.  To accomplish all that I am adding all my scripts and style sheets to _Layout.cshtml to avoid including them in every page.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-2.5.3.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.19.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/customValidation.js")" type="text/javascript"></script>
</head>
<body>
    <div class="page">

You can download entire solution here.

Enjoy.

4 Comments

  1. Pingback: Unobtrusive date compare validator | CopyQuery

Leave a Reply