ASP.NET MVC ships with a handy Compare attribute that allows you to compare two inputs and display a validation message if they do not match. This is great for sign up pages where a password needs to be entered twice and we need to ensure that they are equal but aside from that, I haven’t found any other use for the Compare attribute. What I have found that I have needed more are LessThan and GreaterThan attributes that allow you to compare two inputs and ensure that one is less than or greater than the other. The ASP.NET MVC Framework doesn’t not come loaded with such attributes so using the Compare attribute as a template, I have created them.
You can download a project here that has the complete source code and test pages for these attributes.
We will start first with the validation attribute:
public class NumericLessThanAttribute : ValidationAttribute, IClientValidatable
{
private const string lessThanErrorMessage = "{0} must be less than {1}.";
private const string lessThanOrEqualToErrorMessage = "{0} must be less than or equal to {1}.";
public string OtherProperty { get; private set; }
private bool allowEquality;
public bool AllowEquality
{
get { return this.allowEquality; }
set
{
this.allowEquality = value;
// Set the error message based on whether or not
// equality is allowed
this.ErrorMessage = (value ? lessThanOrEqualToErrorMessage : lessThanErrorMessage);
}
}
public NumericLessThanAttribute(string otherProperty)
: base(lessThanErrorMessage)
{
if (otherProperty == null) { throw new ArgumentNullException("otherProperty"); }
this.OtherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, this.OtherProperty);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo otherPropertyInfo = validationContext.ObjectType.GetProperty(OtherProperty);
if (otherPropertyInfo == null)
{
return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "Could not find a property named {0}.", OtherProperty));
}
object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
decimal decValue;
decimal decOtherPropertyValue;
// Check to ensure the validating property is numeric
if (!decimal.TryParse(value.ToString(), out decValue))
{
return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "{0} is not a numeric value.", validationContext.DisplayName));
}
// Check to ensure the other property is numeric
if (!decimal.TryParse(otherPropertyValue.ToString(), out decOtherPropertyValue))
{
return new ValidationResult(String.Format(CultureInfo.CurrentCulture, "{0} is not a numeric value.", OtherProperty));
}
// Check for equality
if (AllowEquality && decValue == decOtherPropertyValue)
{
return null;
}
// Check to see if the value is greater than the other property value
else if (decValue > decOtherPropertyValue)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
public static string FormatPropertyForClientValidation(string property)
{
if (property == null)
{
throw new ArgumentException("Value cannot be null or empty.", "property");
}
return "*." + property;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationNumericLessThanRule(FormatErrorMessage(metadata.DisplayName), FormatPropertyForClientValidation(this.OtherProperty), this.AllowEquality);
}
}
Like the compare attribute, this attribute will compare against another value on the form. The other property name is passed into the attributes constructor. There is also another named parameter, AllowEquality, which allows you to specify whether or not the value being validated can be equal to the ‘other’ property. The IsValid method is pretty straightforward and compares the two values to see if they are valid.
The last method, GetClientValidationRules creates a ModelClientValidationNumericLessThanRule class defined below:
public class ModelClientValidationNumericLessThanRule : ModelClientValidationRule
{
public ModelClientValidationNumericLessThanRule(string errorMessage, object other, bool allowEquality)
{
ErrorMessage = errorMessage;
ValidationType = "numericlessthan";
ValidationParameters["other"] = other;
ValidationParameters["allowequality"] = allowEquality;
}
}
This class specifies the client validation type and parameters that will be loaded into the data attributes of the input on the html page. Here we have specified that the jQuery client validation type has a name of ‘numericlessthan’ and that it will accept to parameters values named ‘other’ and ‘allowEquality’.
Now that we have created these two classes, we can now generate a model to test the validation:
public class NumericLessThanViewModel
{
public decimal MaxValue { get; set; }
[NumericLessThan("MaxValue", AllowEquality = true)]
[Display(Name="Value")]
public decimal Value { get; set; }
}
For this to work we need at least two properties on the model; one that specifies the maximum value and another that will be used for the user input. On the user input property, add the NumericLessThan attribute and specify the name of the ‘other’ property to which it will be compared and whether or not equality is allowed. The ‘other’ value will usually be loaded as a hidden field in the form.
At this point just the server side validation has been setup. We need to add a javascript file as well to enable client side validation.
jQuery.validator.addMethod('numericlessthan', function (value, element, params) {
var otherValue = $(params.element).val();
return isNaN(value) && isNaN(otherValue) || (params.allowequality === 'True' ? parseFloat(value) <= parseFloat(otherValue) : parseFloat(value) < parseFloat(otherValue));
}, '');
jQuery.validator.unobtrusive.adapters.add('numericlessthan', ['other', 'allowequality'], function (options) {
var prefix = options.element.name.substr(0, options.element.name.lastIndexOf('.') + 1),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(':input[name=' + fullOtherName + ']')[0];
options.rules['numericlessthan'] = { allowequality: options.params.allowequality, element: element };
if (options.message) {
options.messages['numericlessthan'] = options.message;
}
});
function appendModelPrefix(value, prefix) {
if (value.indexOf('*.') === 0) {
value = value.replace('*.', prefix);
}
return value;
}
The first method in the code above adds the actual method that is called when validating the input. In it we check to see if both values are numbers and depending on whether or not we specified to allow equality, we check to ensure that the user input is less than or equal to the other value.
The second method adds the rule to the set of jQuery validation adapters and supplies the wiring up of the parameters that will be supplied to the validation method.
And that is it. The ASP.NET MVC framework and the jQuery validation libraries will take care of the rest.
In the downloadable project above I have included a NumericGreaterThan attribute as well but as you can image, the code is almost identical to the LessThan attribute so I will not be going over it here.