Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC Custom Compare Data Annotation with Client Validation

0.00/5 (No votes)
11 Jun 2014 1  
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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here