Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC unobtrusive range validation of dynamic values

Tags:

I have a value on my model, that must fall within the range of two other values on my model.

For example:

public class RangeValidationSampleModel
{
    int Value { get; set; }

    int MinValue { get; set; }

    int MaxValue { get; set; }
}

Of course, I can't pass these Min/MaxValues into my DataAnnotations attributes, as they have to be constant values.

I'm sure I need to build my own validation attribute, but I haven't done this much and can't wrap my mind around how it should work.

I've searched for about an hour, and have seen all sorts of solutions for building custom validation, but can't find anything to solve this particular problem using MVC3 unobtrusive validation.

like image 813
Jerad Rose Avatar asked Oct 21 '11 14:10

Jerad Rose


People also ask

What is unobtrusive validation in MVC?

An unobtrusive validation in jQuery is a set of ASP.Net MVC HTML helper extensions.By using jQuery Validation data attributes along with HTML 5 data attributes, you can perform validation to the client-side.

How ModelState IsValid works in MVC?

This action method handles the POST operation and when the form is submitted, the object of the PersonModel class is sent to this method. The state of the submitted Model is checked using ModelState. IsValid property and if the Model is valid then the value of the Name property is set in a ViewBag object.

What does ModelState IsValid validate?

ModelState. IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.


2 Answers

You could write a custom validation attribute for this purpose:

public class DynamicRangeValidator : ValidationAttribute, IClientValidatable
{
    private readonly string _minPropertyName;
    private readonly string _maxPropertyName;
    public DynamicRangeValidator(string minPropertyName, string maxPropertyName)
    {
        _minPropertyName = minPropertyName;
        _maxPropertyName = maxPropertyName;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var minProperty = validationContext.ObjectType.GetProperty(_minPropertyName);
        var maxProperty = validationContext.ObjectType.GetProperty(_maxPropertyName);
        if (minProperty == null)
        {
            return new ValidationResult(string.Format("Unknown property {0}", _minPropertyName));
        }
        if (maxProperty == null)
        {
            return new ValidationResult(string.Format("Unknown property {0}", _maxPropertyName));
        }

        int minValue = (int)minProperty.GetValue(validationContext.ObjectInstance, null);
        int maxValue = (int)maxProperty.GetValue(validationContext.ObjectInstance, null);
        int currentValue = (int)value;
        if (currentValue <= minValue || currentValue >= maxValue)
        {
            return new ValidationResult(
                string.Format(
                    ErrorMessage, 
                    minValue,
                    maxValue
                )
            );
        }

        return null;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ValidationType = "dynamicrange",
            ErrorMessage = this.ErrorMessage,
        };
        rule.ValidationParameters["minvalueproperty"] = _minPropertyName;
        rule.ValidationParameters["maxvalueproperty"] = _maxPropertyName;
        yield return rule;
    }
}

and then decorate your view model with it:

public class RangeValidationSampleModel
{
    [DynamicRangeValidator("MinValue", "MaxValue", ErrorMessage = "Value must be between {0} and {1}")]
    public int Value { get; set; }
    public int MinValue { get; set; }
    public int MaxValue { get; set; }
}

then you could have a controller serving a view:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new RangeValidationSampleModel
        {
            Value = 5,
            MinValue = 6,
            MaxValue = 8
        });
    }

    [HttpPost]
    public ActionResult Index(RangeValidationSampleModel model)
    {
        return View(model);
    }
}

and a view of course:

@model RangeValidationSampleModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
    $.validator.unobtrusive.adapters.add('dynamicrange', ['minvalueproperty', 'maxvalueproperty'],
        function (options) {
            options.rules['dynamicrange'] = options.params;
            if (options.message != null) {
                $.validator.messages.dynamicrange = options.message;
            }
        }
    );

    $.validator.addMethod('dynamicrange', function (value, element, params) {
        var minValue = parseInt($('input[name="' + params.minvalueproperty + '"]').val(), 10);
        var maxValue = parseInt($('input[name="' + params.maxvalueproperty + '"]').val(), 10);
        var currentValue = parseInt(value, 10);
        if (isNaN(minValue) || isNaN(maxValue) || isNaN(currentValue) || minValue >= currentValue || currentValue >= maxValue) {
            var message = $(element).attr('data-val-dynamicrange');
            $.validator.messages.dynamicrange = $.validator.format(message, minValue, maxValue);
            return false;
        }
        return true;
    }, '');
</script>

@using (Html.BeginForm())
{
    <div>
        @Html.LabelFor(x => x.Value)
        @Html.EditorFor(x => x.Value)
        @Html.ValidationMessageFor(x => x.Value)
    </div>
    <div>
        @Html.LabelFor(x => x.MinValue)
        @Html.EditorFor(x => x.MinValue)
    </div>
    <div>
        @Html.LabelFor(x => x.MaxValue)
        @Html.EditorFor(x => x.MaxValue)
    </div>
    <button type="submit">OK</button>
}

Obviously the custom adapter registration should be performed in an external javascript file to avoid polluting the view but for the purpose and conciseness of this post I have put it inside the view.

like image 197
Darin Dimitrov Avatar answered Sep 20 '22 07:09

Darin Dimitrov


custom validation attributes are indeed a good thought. something like (digging up some snippet o'mine found who knows where a while ago):

public sealed class MustBeGreaterThan : ValidationAttribute
{
    private const string _defaultErrorMessage = "'{0}' must be greater than '{1}'";
    private string _basePropertyName;

    public MustBeGreaterThan(string basePropertyName)
        : base(_defaultErrorMessage)
    {
        _basePropertyName = basePropertyName;
    }

    //Override default FormatErrorMessage Method
    public override string FormatErrorMessage(string name)
    {
        return string.Format(_defaultErrorMessage, name, _basePropertyName);
    }

    //Override IsValid
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var basePropertyInfo = validationContext.ObjectType.GetProperty(_basePropertyName);
        var lowerBound = (int)basePropertyInfo.GetValue(validationContext.ObjectInstance, null);
        var thisValue = (int)value;

        if (thisValue < lowerBound)
        {
            var message = FormatErrorMessage(validationContext.DisplayName);
            return new ValidationResult(message);
        }

        //value validated
        return null;
    }
}

public sealed class MustBeLowerThan : ValidationAttribute
{
    private const string _defaultErrorMessage = "'{0}' must be lower than '{1}'";
    private string _basePropertyName;

    public MustBeLowerThan(string basePropertyName)
        : base(_defaultErrorMessage)
    {
        _basePropertyName = basePropertyName;
    }

    //Override default FormatErrorMessage Method
    public override string FormatErrorMessage(string name)
    {
        return string.Format(_defaultErrorMessage, name, _basePropertyName);
    }

    //Override IsValid
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var basePropertyInfo = validationContext.ObjectType.GetProperty(_basePropertyName);
        var upperBound = (int)basePropertyInfo.GetValue(validationContext.ObjectInstance, null);
        var thisValue = (int)value;

        if (thisValue > upperBound)
        {
            var message = FormatErrorMessage(validationContext.DisplayName);
            return new ValidationResult(message);
        }

        //value validated
        return null;
    }
}

then decorate your class

public class RangeValidationSampleModel
{
    [MustBeGreaterThan("MinValue")]
    [MustBeLowerThan("MaxValue")]
    int Value { get; set; }

    int MinValue { get; set; }

    int MaxValue { get; set; }
}

and you should be good to go

like image 36
Alex Avatar answered Sep 20 '22 07:09

Alex