Click here to Skip to main content
15,885,842 members
Articles / Web Development / HTML
Tip/Trick

ASP.NET MVC Custom Compare Data Annotation with Client Validation

Rate me:
Please Sign up or sign in to vote.
4.67/5 (12 votes)
11 Jun 2014CPOL1 min read 72.6K   2K   12   5
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:

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

Define the attribute class to compare to properties:

C#
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:

JavaScript
$.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.

C#
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:

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

Here is the view designed for MyModel:

HTML
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)


Written By
Technical Lead
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionSuggestion: write as optimized as possible Pin
John V 20216-May-21 11:04
John V 20216-May-21 11:04 
Questionreturn null??? Pin
John V 20216-May-21 10:09
John V 20216-May-21 10:09 
QuestionPartial Views Pin
Member 1171483126-May-15 9:05
Member 1171483126-May-15 9:05 
AnswerRe: Partial Views Pin
John V 20216-May-21 10:06
John V 20216-May-21 10:06 
GeneralMy vote of 4 Pin
Duncan Edwards Jones11-Jun-14 3:23
professionalDuncan Edwards Jones11-Jun-14 3:23 

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

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