11

Let's say I have this view model:


    public class MyModel
    {
        [Range(0, 999, ErrorMessage = "Invalid quantity")]
        public int Quantity { get; set; }
    }

Now, for specific instances of this model the range of valid values will change: some may not be 0, some may not be higher than 5. The min/max values for the valid ranges are coming from the DB and can change anytime.

How do I change the min/max properties of the RangeAttribute on the fly? Or what it the best way to validate my scenario?

tereško
  • 58,060
  • 25
  • 98
  • 150
Andrei
  • 261
  • 4
  • 11
  • I wont submit this as an answer but; I had this problem last week, after a ton of reading it turns out the attribute properties are required to be constants. There was a solution to using dynamic values using a CustomValidationAttribute class i think. Link: http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.customvalidationattribute%28VS.100%29.aspx – M05Pr1mty Feb 15 '12 at 16:13
  • I think I am going to try to combine Shark's and Andrei's answers below and create a custom attribute that uses the additional min/max properties from the model to validate the quantity field... – Andrei Feb 15 '12 at 16:51
  • See my edited answer for a working and complete example and implementation. –  Feb 15 '12 at 17:20
  • What I ended up doing -- I added the min and max properties to the view model (as Shark suggested), implemented IValidatableObject in the view model and put validation into Validate method. It works and it is clean enough, but then the link posted by Matt has even better solution: validation happens on the client side and I can change the ranges for my model's instances. So I guess Matt is getting the credit for answering both of my questions. Thank you! – Andrei Feb 16 '12 at 23:45
  • Can you tell the namespace to be used/or to add any reference for "`IClientValidatable`", actually, right now Complier is not recognizing it. I am using VS 2010. I added a new project and the project type used is - "**ASP.Net MVC2 Web Application Visual C#**" – Pankaj Feb 17 '12 at 08:36

2 Answers2

10

Something along the lines of this might be more what your after...

ViewModel:

public class ViewModel
{
    public DateTime MinDate {get; set;}
    public DateTime MaxDate {get; set;}

    [DynamicRange("MinDate", "MaxDate", ErrorMessage = "Value must be between {0} and {1}")]
    public DateTime Date{ get; set; }
}

Library class or elsewhere:

public class DynamicRange : ValidationAttribute, IClientValidatable
    {
        private readonly string _minPropertyName;
        private readonly string _maxPropertyName;

    public DynamicRange(string minPropName, string maxPropName)
    {
        _minPropertyName = minPropName;
        _maxPropertyName = maxPropName;
    }

    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));

        var minValue = (int) minProperty.GetValue(validationContext.ObjectInstance, null);
        var maxValue = (int) maxProperty.GetValue(validationContext.ObjectInstance, null);

        var 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 = ErrorMessage
            };

        rule.ValidationParameters["minvalueproperty"] = _minPropertyName;
        rule.ValidationParameters["maxvalueproperty"] = _maxPropertyName;
        yield return rule;
    }

From: MVC unobtrusive range validation of dynamic values

Community
  • 1
  • 1
M05Pr1mty
  • 731
  • 9
  • 29
  • Can you tell the namespace to be used/or to add any reference for "IClientValidatable", actually, right now complier is not recognizing it. – Pankaj Feb 17 '12 at 08:31
  • Can you tell the namespace to be used/or to add any reference for "`IClientValidatable`", actually, right now Compiler is not recognizing it. I am using VS 2010. I added a new project and the project type used is - "**ASP.Net MVC2 Web Application Visual C#**" – Pankaj Feb 17 '12 at 08:37
  • http://msdn.microsoft.com/en-us/library/system.web.mvc.iclientvalidatable(v=vs.98).aspx – M05Pr1mty Feb 18 '12 at 10:22
1

I think your best bet might be to implement a custom model binder for your specific Model (MyModel). What you could have is something like this:

public class MyModel
{
    public int Quantity { get; set; }
} // unchanged Model

public class MyViewModel
{
    public MyModel myModel { get; set; }
    public int QuantityMin { get; set; }
    public int QuantityMax { get; set; }
}

Then you can set these values, and in your custom model binder you can compare your myModel.Quantity property to the QuantityMin and QuantityMax properties.


Example

Model:

public class QuantityModel
{
    public int Quantity { get; set; }
}

ViewMode:

public class QuantityViewModel
{
    public QuantityModel quantityModel { get; set; }
    public int QuantityMin { get; set; }
    public int QuantityMax { get; set; }
}

Custom Model Binder:

public class VarQuantity : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        int MinValue = Convert.ToInt32(bindingContext.ValueProvider.GetValue("QuantityMin").AttemptedValue);
        int MaxValue = Convert.ToInt32(bindingContext.ValueProvider.GetValue("QuantityMax").AttemptedValue);
        int QuantityValue = Convert.ToInt32(bindingContext.ValueProvider.GetValue("quantityModel.Quantity").AttemptedValue);

        if (!(QuantityValue >= MinValue && QuantityValue <= MaxValue))
            bindingContext.ModelState.AddModelError("Quantity", "Quantity not between values");

        return bindingContext.Model;
    }
}

Register Custom Model Binder:

ModelBinders.Binders.Add(typeof(QuantityViewModel), new VarQuantity());

Test Controller Action Methods:

    public ActionResult Quantity()
    {
        return View();
    }

    [HttpPost]
    public string Quantity(QuantityViewModel qvm)
    {
        if (ModelState.IsValid)
            return "Valid!";
        else
            return "Invalid!";
    }

Test View Code:

@model MvcTest.Models.QuantityViewModel

<h2>Quantity</h2>

@using (Html.BeginForm())
{
    @Html.Label("Enter Your Quantity: ")
    @Html.TextBoxFor(m => m.quantityModel.Quantity)
    <br />
    @Html.Label("Quantity Minimum: ")
    @Html.TextBoxFor(m => m.QuantityMin)
    <br />
    @Html.Label("Quantity Maximum: ")
    @Html.TextBoxFor(m => m.QuantityMax)
    <br /><br />
    <input type="submit" value="Submit" />
}
Community
  • 1
  • 1
  • This is not the correct answer. OP is interesting to know whether it is possible to do it on the fly ? – Pankaj Feb 15 '12 at 16:48
  • @PankajGarg It is possible to do it on the fly. See my edit and my working example. This is surely a way. –  Feb 15 '12 at 17:09