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.
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 GenericCompareAttribute() : base() { }
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) {
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
{
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: