Manipulate the model value before passing it to DefaultModelBinder.BindModel

Some properties decimal, and decimal?in my view a model as a data type labeled "Percent", together with other data annotations, such as:

[DataType("Percent")]
[Display(Name = "Percent of foo completed")]
[Range(0, 1)]
public decimal? FooPercent { get; set; }

I would like to give the user some flexibility in the way they enter data, i.e. with or without percent sign, intermediate spaces, etc. But I still want to use the behavior DefaultModelBinderto get all its functions, for example, checking RangeAttributeand adding the appropriate verification messages.

Is there a way to parse and change the value of the model and then pass it on? This is what I am trying, but getting an exception at runtime. (Ignore the actual parsing logic, this is not its final form. I am interested in the issue of replacing the model at the moment.)

public class PercentModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelMetadata.DataTypeName == "Percent")
        {
            ValueProviderResult result =
                bindingContext.ValueProvider.GetValue(
                    bindingContext.ModelName);
            if (result != null)
            {
                string stringValue =
                    (string)result.ConvertTo(typeof(string));
                decimal decimalValue;
                if (!string.IsNullOrWhiteSpace(stringValue) &&
                    decimal.TryParse(
                        stringValue.TrimEnd(new char[] { '%', ' ' }),
                        out decimalValue))
                {
                    decimalValue /= 100.0m;

                    // EXCEPTION : This property setter is obsolete, 
                    // because its value is derived from 
                    // ModelMetadata.Model now.
                    bindingContext.Model = decimalValue;
                }
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}
+5
source share
1 answer

Nothing, it was a fundamental misunderstanding of where validation occurs in the MVC loop. After spending some time in the MVC source code, I see how this works.

In case this is useful to others, here is what works for me:

[DataType("Percent")]
[Display(Name = "Percent of foo completed")]
[Range(0.0d, 1.0d, ErrorMessage="The field {0} must be between {1:P0} and {2:P0}.")]
public decimal? FooPercent { get; set; }

And in the insert, you simply return the value:

public class PercentModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelMetadata.DataTypeName == "Percent")
        {
            ValueProviderResult result =
                bindingContext.ValueProvider.GetValue(
                    bindingContext.ModelName);
            if (result != null)
            {
                string stringValue =
                    (string)result.ConvertTo(typeof(string));
                decimal decimalValue;
                if (!string.IsNullOrWhiteSpace(stringValue) &&
                    decimal.TryParse(
                        stringValue.TrimEnd(new char[] { '%', ' ' }),
                        out decimalValue))
                {
                    return decimalValue / 100.0m;
                }
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}
+5
source

All Articles