ASP.NET MVC: LessThan and GreaterThan Validation Attributes


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.

About these ads

8 Responses to “ASP.NET MVC: LessThan and GreaterThan Validation Attributes”

  1. Mike Fedosenko Says:

    Hi Nick,
    Thanks for validation attributes, they fitted my project just fine.
    As an improvement I offer to use some display name of OtherProperty in error message.
    If you mark other property in your view model with [Display(Name="User friendly name")], the following code would be able to retrieve this text:

    private static string GetOtherPropertyDisplayName(PropertyInfo property) {
    var displayAttribute = property
    .GetCustomAttributes(typeof(DisplayAttribute), false)
    .OfType()
    .FirstOrDefault();

    return displayAttribute == null ? property.Name : displayAttribute.Name;
    }

    Take the display name of another property, store in private field and reuse in FormatErrorMessage method. Done.
    ======
    Just discovered a problem with jQuery selector, through which JS code searches for other property. In case of other property is a property of an object in array, it would get name like Books[1].OtherProperty. Special characters in jQuery selectors should be escaped. Easily done with escape function from SO:
    http://stackoverflow.com/questions/739695/jquery-selector-value-escaping
    I added the function under name ‘escapeSelector’ and used like this:
    fullOtherName = appendModelPrefix(other, prefix);
    fullOtherName = escapeSelector(fullOtherName);

    function escapeSelector(input) {
    console.info(“escapeSelector: ” + input);
    if (input) {
    var result = input.replace(/([ #;&,.+*~\':"!^$[\]()=>|\/@])/g, ‘\\$1′);
    console.info(“escaped: ” + result);
    return result;
    } else return input;
    }

    Have a nice day.

    • Nick Olsen Says:

      Thanks for the contribution. The original attribute and the one that I use in production actually gets the display name of the other property and uses it in the formatted error message as well but I went about it a little bit differently. I will post that version as well to compare.

      • longpshorn Says:

        Hi Nick! Thanks for the post. Excellent stuff and has helped me in my project tremendously. I am trying to get the display name to automatically display as discussed on this thread but having trouble with Mike’s method. Was wondering if you had a chance to post your method yet.

        Thanks again!
        - Patrick

  2. Mike Says:

    Hi Nick,

    Sorry for the unrelated Reply, but I had a question on something you had posted on stackoverflow.com regarding Editing a Variable Length List, ASP.NET MVC 3 Style with Table. You mentioned that you had converted Steve’s MV2 solution MVC3 Razor. I don’t suppose you have that posted anywhere?

    Thanks,

    Mike

    • Nick Olsen Says:

      You can find the solution on this SO answer: http://stackoverflow.com/a/7180568/489213

  3. Steve Says:

    Thanks for putting in the work to share this post, I just added to my project and it works well. I ran into two issues that I’d like to share the solution for. The first is although this was working for a little while in my project it stopped because of this line in the javascript:

    element = $(options.form).find(‘:input[name="' + fullOtherName + '"]‘)[0];

    It was selecting the first input element on the form and not looking at the name attribute, the fix is to add double quotes before and after the fullOtherName variable reference which is I added to the line above. I’m a little confused though why it was working before it may be due to the fact that we upgraded to jquery version 1.7.1.

    The second issue is related to using the ASP.NET MVC display name annotation of the other property. I attempted to use Mikes suggestion but also couldn’t get it to work as i couldn’t figure out how to hook into the other property at the constructor level. I did use Mikes original method but slightly altered:

    private void GetOtherPropertyDisplayName(PropertyInfo property)
    {
    var displayAttribute = ((DisplayAttribute)property
    .GetCustomAttributes(typeof(DisplayAttribute), false)
    .FirstOrDefault());

    if (displayAttribute != null && !string.IsNullOrEmpty(displayAttribute.Name))
    OtherPropertyDisplayName = displayAttribute.Name;
    }
    I added this property:

    public string OtherPropertyDisplayName { get; set; }

    Than I set it in GetClientValidationRules method like:

    public IEnumerable GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
    if (metadata != null)
    {
    if (!string.IsNullOrEmpty(metadata.ContainerType.FullName))
    {
    Type model = Type.GetType(metadata.ContainerType.FullName);
    if (model != null)
    {
    PropertyInfo propInfo = model.GetProperty(OtherProperty);
    GetOtherPropertyDisplayName(propInfo);
    }
    }
    yield return
    new ModelClientValidationNumericGreaterThanRule(FormatErrorMessage(metadata.DisplayName),
    FormatPropertyForClientValidation(OtherProperty),
    AllowEquality);
    }
    }

    I simply add a line right after getting the propertyinfo in IsValid method:

    GetOtherPropertyDisplayName(otherPropertyInfo);

    This unfortunately is the best solution I could come up with I”m hoping maybe someone can shed some light on how to do this a better way like inside of the constructor, I couldn’t figure out how to gain access to the assembly name of the other property from the constructor. This down side to my implementation is that it will only work if the other property is on the same class or model of the property the annotation was added to.

    Thanks for your great help!

  4. Shema Says:

    Hi Nick,

    This is great thanks…

    I only have one question, I added this to the project and it works great the only issue I have is that if I leave one or both of the two fields (the ones I am comparing) empty it still dose the compare and shows the validation. However, I would like it to only compare if the two fields are filled in.

    Thanks

    • Shema Says:

      If anyone else has the same issue I fixed it…

      In order to fix it, the JavaScript needs changing to:
      jQuery.validator.addMethod(‘numericlessthan’, function (value, element, params) {
      var otherValue = $(params.element).val();
      //Add this line
      if ((value == “” || otherValue == “”)) { return true}

      return isNaN(value) && isNaN(otherValue) || (params.allowequality === ‘True’ ? parseFloat(value) <= parseFloat(otherValue) : parseFloat(value) decOtherPropertyValue)
      {
      return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
      }
      }
      return null;

      }


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 66 other followers

%d bloggers like this: