Click here to Skip to main content
Click here to Skip to main content

ASP.NET MVC Custom Compare Data Annotation with Client Validation

, 11 Jun 2014
Rate this:
Please Sign up or sign in to vote.
This is a tip to add custom data annotation with client validation in ASP.NET MVC 5

Introduction

This is a tip to add custom compare data annotation with client validation in ASP.NET MVC 5. The main objective is to provide the comparison validation between two properties of a viewmodel or two similar columns in same form using <, > <=, >= operators for the datatype of numbers and datetimes.

edit

Using the Code

I am creating a small ASP.NET MVC 5 app and creating a custom attribute class by inheriting System.ComponentModel.DataAnnotations.ValidationAttribute to put base validation logic and System.Web.Mvc.IClientValidatable to render validation attributes to elements.

Tested Environment

  • Visual Studio 2013
  • ASP.NET MVC 5
  • jquery-1.10.2.js
  • jquery.validate.js
  • Integers, real numbers, date and time data types

Add compare operators as enum.

genericcompare.cs in .NET C# 5:

public enum GenericCompareOperator
    {
        GreaterThan,
        GreaterThanOrEqual,
        LessThan,
        LessThanOrEqual
    }

Define the attribute class to compare to properties:

public sealed class GenericCompareAttribute : ValidationAttribute, IClientValidatable
    {
        private GenericCompareOperator operatorname = GenericCompareOperator.GreaterThanOrEqual;

        public string CompareToPropertyName { get; set; }
        public GenericCompareOperator OperatorName { get { return operatorname; } set { operatorname = value; } }
        // public IComparable CompareDataType { get; set; }

        public GenericCompareAttribute() : base() { }
        //Override IsValid
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            string operstring = (OperatorName == GenericCompareOperator.GreaterThan ? 
            "greater than " : (OperatorName == GenericCompareOperator.GreaterThanOrEqual ? 
            "greater than or equal to " : 
            (OperatorName == GenericCompareOperator.LessThan ? "less than " : 
            (OperatorName == GenericCompareOperator.LessThanOrEqual ? "less than or equal to " : ""))));
            var basePropertyInfo = validationContext.ObjectType.GetProperty(CompareToPropertyName);

            var valOther = (IComparable)basePropertyInfo.GetValue(validationContext.ObjectInstance, null);

            var valThis = (IComparable)value;

            if ((operatorname == GenericCompareOperator.GreaterThan && valThis.CompareTo(valOther) <= 0) ||
                (operatorname == GenericCompareOperator.GreaterThanOrEqual && valThis.CompareTo(valOther) < 0) ||
                (operatorname == GenericCompareOperator.LessThan && valThis.CompareTo(valOther) >= 0) ||
                (operatorname == GenericCompareOperator.LessThanOrEqual && valThis.CompareTo(valOther) > 0))
                return new ValidationResult(base.ErrorMessage);
            return null;
        }
        #region IClientValidatable Members

        public IEnumerable<ModelClientValidationRule> 
        GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
            ModelClientValidationRule compareRule = new ModelClientValidationRule();
            compareRule.ErrorMessage = errorMessage;
            compareRule.ValidationType = "genericcompare";
            compareRule.ValidationParameters.Add("comparetopropertyname", CompareToPropertyName);
            compareRule.ValidationParameters.Add("operatorname", OperatorName.ToString());
            yield return compareRule;
        }

        #endregion
    }

customannotation.js:

$.validator.addMethod("genericcompare", function (value, element, params) {
    // debugger;
    var propelename = params.split(",")[0];
    var operName = params.split(",")[1];
    if (params == undefined || params == null || params.length == 0 || 
    value == undefined || value == null || value.length == 0 || 
    propelename == undefined || propelename == null || propelename.length == 0 || 
    operName == undefined || operName == null || operName.length == 0)
        return true;
    var valueOther = $(propelename).val();
    var val1 = (isNaN(value) ? Date.parse(value) : eval(value));
    var val2 = (isNaN(valueOther) ? Date.parse(valueOther) : eval(valueOther));
   
    if (operName == "GreaterThan")
        return val1 > val2;
    if (operName == "LessThan")
        return val1 < val2;
    if (operName == "GreaterThanOrEqual")
        return val1 >= val2;
    if (operName == "LessThanOrEqual")
        return val1 <= val2;
})
;$.validator.unobtrusive.adapters.add("genericcompare", 
["comparetopropertyname", "operatorname"], function (options) {
    options.rules["genericcompare"] = "#" + 
    options.params.comparetopropertyname + "," + options.params.operatorname;
    options.messages["genericcompare"] = options.message;
});

Below is the viewmodel class to apply the annotation to compare EndDate with StartDate property, and compares NumTo with NumFrom., Error message either to mention in resources and refer the at attribute or specify the errormessage in attribute using ErrorMessage property.

public class mymodel
{
[Display(Name = "Start Date:")]
[DataType(DataType.Date)]       
public DateTime? StartDate { get; set; }

[Display(Name = "End Date:")]
[DataType(DataType.Date)]    
[GenericCompare(CompareToPropertyName= "StartDate",OperatorName= GenericCompareOperator.GreaterThanOrEqual,ErrorMessageResourceName="resourcekey", 
ErrorMessageResourceType=typeof(resourceclassname))]
public DateTime? EndDate { set; get; }

[Display(Name = "Number From:")]
public int? NumFrom { get; set; }

[Display(Name = "Number To:")]
[GenericCompare(CompareToPropertyName = "NumFrom", 
OperatorName = GenericCompareOperator.GreaterThanOrEqual, 
ErrorMessageResourceName = "resourcekey", 
ErrorMessageResourceType = typeof(resourceclassname))]
public int? NumTo { set; get; }
}

Write the test controller class:. there is index.cshtml in views of this controller:

public class TestCompareController : Controller
{
  //
        // GET: /TestCompare/
        public ActionResult Index()
        {
            return View();
        }
}

Here is the view designed for MyModel:

Index.cshtml

@model customcompare_MVC.Models.MyModel

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken() <div class="form-horizontal">
        <h4>MyModel</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(model => model.StartDate, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.StartDate)
                @Html.ValidationMessageFor(model => model.StartDate)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.EndDate, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.EndDate)
                @Html.ValidationMessageFor(model => model.EndDate)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.NumFrom, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.NumFrom)
                @Html.ValidationMessageFor(model => model.NumFrom)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.NumTo, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.NumTo)
                @Html.ValidationMessageFor(model => model.NumTo)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@*script section defined in views/shared/_layout.cshtml*@
@section Scripts {
   @*This bundle created in App_Start/bundleconfig.cs and registered the bundle in application_start event in global.asax.cs*@
    @Scripts.Render("~/bundles/jqueryval")
    @Scripts.Render("~/Scripts/customcompare.js")
}

Run the application, end date shows the error if the end date is less than to start date, Number to show error if Number To is greater than or equal to Number From.

Invalid entries:

Valid entries:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sreenivas Chinni
Software Developer (Senior)
India India
No Biography provided

Comments and Discussions

 
GeneralMy vote of 4 PinpremiumDuncan Edwards Jones11-Jun-14 3:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140821.2 | Last Updated 11 Jun 2014
Article Copyright 2014 by Sreenivas Chinni
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid