Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Manipulate model value before passing it to DefaultModelBinder.BindModel

Some decimal and decimal? properties in my view model are marked as "Percent" data type, along with other data annotations, for example:

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

I'd like to permit the user some flexibility in how they enter the data, i.e. with or without the percent sign, intermediate spaces, etc. But I still want to use the DefaultModelBinder behavior to get all of its functionality such as checking the RangeAttribute and adding the appropriate validation messages.

Is there a way to parse and change the model value, then pass it along? Here is what I am trying, but am getting a runtime exception. (Ignore the actual parsing logic; this is not its final form. I'm just interested in the model replacement question at this point.)

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);
    }
}
like image 439
Dave Mateer Avatar asked Jan 04 '12 15:01

Dave Mateer


1 Answers

Never mind, this was a fundamental misunderstanding of where validation happens in the MVC cycle. After spending some time in the MVC source code, I see how this works.

In case it is helpful to others, here is what is working 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 binder, you just 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);
    }
}
like image 94
Dave Mateer Avatar answered Nov 17 '22 10:11

Dave Mateer