Iterating on an ASP.NET MVC Model Binder

After my last post on model binding tips I’ve had a number of questions about the nuances of model binding. Let’s work through a sample.

Imagine you have a Recipe class that can hold all the information you need to make Love Mussels, and now you've decided to build a model binder to bind and validate recipes. 

Iteration 1

Let’s start with the naive approach. This code has a number of problems.

public class RecipeModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var form = controllerContext.HttpContext.Request.Form;

        var recipe = new Recipe();            
        recipe.Name = form["Name"];
        // ... and so on for all properties

        if (String.IsNullOrEmpty(recipe.Name))
        {
            bindingContext.ModelState.AddModelError("Name", "...");
        }

        return recipe;
    }
}

Problem

The code works directly off the HttpContext.Request.Form.

There are a couple difficulties working with the Form collection directly. One problem is how your tests will require more setup to create an HttpContext, Request, and Form objects.

The second problem is related to culture. Values you need from the Form collection are culture sensitive because the user will type a date and time value into their browser using a local convention. However, the URL is another place you might need to check when binding values (the query string and routing data in general), and these values are culture invariant.

Instead of worrying about all these details, it’s better to use the ValueProvider given to us by the incoming binding context. The value provider is easy to populate in a unit test, and takes care of culture sensitive conversions.

Iteration 2

We’ll add a GetValue method to help fetch values from the ValueProvider. At runtime the MVC framework populates the provider with values it finds in the request’s form, route, and query string collections.

public class RecipeModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var recipe = new Recipe();
        recipe.Name = GetValue<string>(bindingContext, "Name");
        // ... and so on for all properties

        if (String.IsNullOrEmpty(recipe.Name))
        {
            bindingContext.ModelState.AddModelError("Name", "...");
        }

        return recipe;
    }

    private T GetValue<T>(ModelBindingContext bindingContext, string key) 
    {
        ValueProviderResult valueResult;
        bindingContext.ValueProvider.TryGetValue(key, out valueResult);            
        return (T)valueResult.ConvertTo(typeof(T));
    }  
}

Problem:

If you use any HTML Helpers (like Html.TextBox), you’ll see null reference exceptions when validation errors are present.

One of the side-effects of model binding is that binding the model should put model values into ModelState. When an HTML helper sees there is a ModelState error for “Name”, it assumes it will also find the “attempted value” that the user entered. The helper uses attempted values to repopulate inputs and allow the user to fix any errors.

Iteration 3

The only change is to set the model value inside GetValue.

private T GetValue<T>(ModelBindingContext bindingContext, string key)
{
    ValueProviderResult valueResult;
    bindingContext.ValueProvider.TryGetValue(key, out valueResult);
    bindingContext.ModelState.SetModelValue(key, valueResult);       
    return (T)valueResult.ConvertTo(typeof(T));
}  

Problem:

The model binder we’ve written so far will work with controller actions like the following:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Recipe newRecipe)
{
    // ...
    return View(newRecipe);
}

But it won’t work in this scenario:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection formCollection)
{
    var recipe = RecipeFactory.Create();
    TryUpdateModel(recipe);
    if (ModelState.IsValid)
    {
        // save it ...
    }
    return View(recipe);
}

The recipe in the controller action will never see any changes when calling TryUpdateModel, because the model binder is creating and binding data into its own recipe object. Perhaps you never use UpdateModel or TryUpdateModel, but if you expect your model binder to work in any possible situation, you need to make sure the model binder works when someone else creates the model.

Iteration 4

The only change is to check bindingContext.Model to see if we already have a model. Only when this property is null will we go to the trouble of creating a new model. 

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    var form = controllerContext.HttpContext.Request.Form;

    var recipe = (Recipe)(bindingContext.Model ?? new Recipe());
    recipe.Name = GetValue<string>(bindingContext, "Name");
    
    // ... 

    return recipe;
}

Problem:

Congratulations! We just re-implemented the behavior of the DefaultModelBinder - only our model binder is stupid and only works with Recipes.

If all we need is validation for a specific type of model, we can derive from the built-in binder and override OnModelUpdated or OnPropertyValidating and provide our custom logic.

Iteration 5

OnModelUpdated is easy to work with, so what follows is the entire listing for our custom model binder.

public class RecipeModelBinder : DefaultModelBinder 
{
    protected override void OnModelUpdated(ControllerContext controllerContext,
                                           ModelBindingContext bindingContext)
    {
        var recipe = bindingContext.Model as Recipe;
        if (String.IsNullOrEmpty(recipe.Name))
        {
            bindingContext.ModelState.AddModelError("Name", "...");
        }
    }
}

It turns out we didn’t need any code from those first four iterations, but I hope you find them useful because they demonstrate some common problems I’m seeing in custom model binders. Of course we could take this example even further and eliminate the magic strings, but we’ll leave the work for another day.

In software development – every iteration is a learning opportunity!

Print | posted @ Tuesday, May 05, 2009 10:49 PM

Comments on this entry:

Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by Yazid at 5/6/2009 8:19 PM

Excellent article,

var recipe = bindingContext.Model as Recipe;

recipe can be null and thus you will get an exception when you call recipe.Name.

Cheers
Yaz


  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by Scott at 5/6/2009 8:27 PM

@Yazid:

Yes, that's true.

At that point in the code I think I'd leave it as an exceptional circumstance - if model is null or not a recipe in OnModelUpdated there is a serious problem!
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by suhair at 5/7/2009 8:25 AM

Informative, Thanks
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by MikeS at 5/10/2009 9:00 AM

Yazid's point is valid, though incomplete. The issue is using a safe cast (with "as") instead of a type cast. I've seen this mistake creeping into C# sample code all over the blogosphere.

It's fundamentally wrong. You should do a straight cast (var recipe = (Recipe) bindingContext.Model) so that your code fails fast and you get an informative exception (invalid cast) instead of a confusing one sometime later (null reference).
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by scott at 5/10/2009 12:49 PM

@MikeS -

I can type cast a null reference into any reference type, so I don't think this works the way you expect.

In other words, the following code does not throw an invalid InvalidCastException.

object foo = null;
string bar = (string)foo;

I'll leave the "as" for now.
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by Bryan at 5/14/2009 4:56 PM

Scott, as nit-picky as it may be, they are write about the cast operation and you are wrong.
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by Scott Allen at 5/14/2009 5:34 PM

@Bryan:

I think you have to look at the bigger picture.

This binder is configured into the runtime to bind a specific type (a Recipe). The code will never be invoked for something that is not already a Recipe.

OnModelUpdated is invoked after the model is populated, thus the model will never be null. Even if it was null, an explcit cast operator doesn't help. The runtime will casts a null to any reference type.

I don't see why we have to sacrifice readability to cover scenarios that will never happen.

What am I missing?
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by Ross at 11/11/2009 9:28 AM

Great article thanks. There isn't much info out there on how the model binders actually work.
  
Gravatar # re: Iterating on an ASP.NET MVC Model Binder
by mike at 3/7/2010 5:43 AM

Very helpful, so little information on binders out there. Thanks for the write-up.
  

Your comment:

Title:
Name:
Email:
Website:
 
Italic Underline Blockquote Hyperlink
 
 
Please add 4 and 1 and type the answer here: