20

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.

Jerad Rose
  • 15,235
  • 18
  • 82
  • 153
  • does this have to be client side validation? – Adam Tuliper Oct 21 '11 at 14:59
  • 1
    It would be preferable. We're converting this site from MVC2 to MVC3, and currently, the MVC2 validation is working client side, so I'd like to keep it working that way. But I'd like to use unobtrusive validation, if possible. Current validation is very obtrusive. :) – Jerad Rose Oct 21 '11 at 15:09

3 Answers3

37

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.

Raj Bhagat
  • 13
  • 3
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Thanks Darin, this is awesome! I did have to make two small tweaks to the javascript code. The `isNaN` checks were reversed (had to remove the `!`), and I also moved the min/max check w/ the `isNaN` checks so that the message would get set properly. – Jerad Rose Oct 22 '11 at 03:32
  • @JeradRose, indeed, my code had a mistake. I have fixed it now. You should not hesitate to update my answer as well if you see issues with it. – Darin Dimitrov Oct 22 '11 at 08:58
  • thanks for that. I considered editing it, but it looks like I don't have permissions to do that yet (I think that's at 2k). – Jerad Rose Oct 22 '11 at 22:15
  • 2
    I tried your solution and found one serious issue: if I more than one properties are decorated with your custom attribute, each property is set to use different error message. When both/all of the properties fail validation, the exact same message show up for all of them. The reason is your JavaScript line `$.validator.messages.dynamicrange = $.format($.validator.messages.dynamicrange, minValue, maxValue);` replaces the message texts for all. So far I have not found a good solution yet. – hardywang Feb 22 '12 at 19:12
  • 2
    @hardywang Had exact problem as you. Not sure if I've got around it but I used $(element).attr('data-val-dynamicrange') to get the error message per element and set the message using that. – dreza Oct 03 '12 at 00:33
  • @dreza eventually I gave up and turned to use remote validation. – hardywang Oct 03 '12 at 01:41
  • 1
    +1 to Darin for the recommendation but I would throw an exception if the Property doesn't exist in the model. That's an error that I would like to log to let me or any other developers that there's an issue with the validation. – JustinMichaels Oct 30 '12 at 20:24
  • 3
    I tend to over-analyze these sorts of things but one thing that jumped out at me was this solution could lead to unintended consequences. If your value is an actual input on the screen that you allow to be edited then this will work for you. If you are making the min and max value properties hidden inputs then you could potentially be opening yourself up to the server-side validation being circumvented. \\ – JustinMichaels Oct 31 '12 at 18:11
  • Great great great Darin... I'm using it. As @JustinMichaels points out, the validation can be circunvented on the client side... despite that, you should always perform the validation on the server side too. – Leniel Maccaferri Aug 22 '13 at 16:44
  • I assumed that server-side validation would be performed, if client side validation is circumvented, but do not see it work. – R. Schreurs Mar 18 '16 at 13:51
3

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

Alex
  • 23,004
  • 4
  • 39
  • 73
  • Thanks @alex. This doesn't look like it would work with client-side unobtrusive validation, though, will it? – Jerad Rose Oct 21 '11 at 15:06
0

If you need client side validation this will have to be custom. I saw a nice post on here recently (Darin Dmitrov? Cant seem to find it) Anyways - this will allow client validation to occur: http://blogs.msdn.com/b/simonince/archive/2011/02/04/conditional-validation-in-asp-net-mvc-3.aspx

Server side can be handled via IValidateableObject or Dynamic Range Validation in ASP.NET MVC 2

etc etc on the server side but I feel you want client side here to be the key.

Community
  • 1
  • 1
Adam Tuliper
  • 29,982
  • 4
  • 53
  • 71