Custom model binding using IModelBinder in ASP.NET MVC – two gotchas

There are a few blog posts around the web regarding ASP.NET MVC custom model binding, but there are two key pieces of information missing.

Just a quick intro – to implement custom model binding in ASP.NET MVC we need to implement the IModelBinder interface:

public interface IModelBinder
{
    object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
}

For the sake of the example let’s say we have a class (below) that doesn’t have a public constructor and for one or another reason we don’t want to have a view model.

public class Quantity
{
    public Quantity (float value, MeasurementUnits units)
    {
          this.Value = value;
          this.Units = units;
    }

    public float Value { get; set; }
    public MeasurementUnits Units { get; set; }
}

public enum MeasurementUnits  { mg, g, kg, l, kcal }

With this class in ASP.NET MVC the model binding won’t work, because it doesn’t expose a public constructor. We have three options:

  1. “Mirror” the Quantity class in a View Model class, but that also requires mirroring each class that has a property of type Quantity as well
  2. Manually build an instance by retrieving the units and value from the Request parameters. However this will have to be done in each controller action (code duplication is bad).
  3. Use custom model binding.

Here is the model binder:

public class QuantityModelBinder : IModelBinder
{
    public object BindModel (ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (controllerContext == null)
            throw new ArgumentNullException ("controllerContext", "controllerContext is null.");
        if (bindingContext == null)
            throw new ArgumentNullException ("bindingContext", "bindingContext is null.");

        MeasurementUnits? units = TryGet<MeasurementUnits> (bindingContext, "Units");
        float? value = TryGet<float> (bindingContext, "Value");

        if (units.HasValue && value.HasValue)
            return new Quantity (value.Value, units.Value);

        return null;
    }

    private Nullable<T> TryGet<T> (ModelBindingContext bindingContext, string key) where T : struct
    {
        if (String.IsNullOrEmpty (key))
            return null;

        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + "." + key);
        if (valueResult == null && bindingContext.FallbackToEmptyPrefix == true)
            valueResult = bindingContext.ValueProvider.GetValue (key);

        bindingContext.ModelState.SetModelValue (bindingContext.ModelName, valueResult);

        if (valueResult == null)
            return null;

        try {
            return (Nullable<T>)valueResult.ConvertTo (typeof (T));
        } catch (Exception ex) {
            bindingContext.ModelState.AddModelError (bindingContext.ModelName, ex);
            return null;
        }
    }
}

The two important gotchas are:

  1. If your binding validation fails you should log an appropriate validation error via ModelState.AddModelError . It is useful to know that if you add exceptions of type FormatException to the model errors, ASP.NET MVC is smart enough to automatically convert them to string errors post-binding but before it returns control to your controller action.
  2. If  you log an error you must call ModelState.SetModelValue or otherwise if you return null it will cause a NullReferenceException in the default ASP.NET MVC binder. This is so because unless you call that method the rest of the ASP.NET MVC code (as well as your own code) won’t be able to access ModelState[propertyName] and because it doesn’t handle that it blows up.

P.S:  If I have to be pedantic and the code path is not critical I won’t use strings in the model binder to refer to the Quantity properties. Instead I would use something like what I’ve written about in will use the method described in How to avoid passing property names as strings using C# 3.0 Expression Trees .

Be Sociable, Share!
  • René

    THANK YOU! I’ve made a custom model binder and couldn’t figure out why the user provided value would disappear if my model binder added a model error. But thanks to your post, I found out I was missing a call to SetModelValue. Thank you so much!

  • http://www.hospiceoxnard.com Hospice oxnard

    This is still a little confusing to me.
    Are there any youtube videos that could help me understand custom model binders better?

  • amazed

    I am trying to deal with a situation where I handle an exception in custom model binder and after which I loose the model values in controller. In above code i understand that units field is being validation however you are setting the SetModelValue to the ‘Units’ field only. In my case I am missing all field values in controller for the Model. My Model is Huge(over 30 properties). Do I need to do Setmodelvalue for all of them?