Iterating on an ASP.NET MVC Model Binder

Tuesday, May 5, 2009

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!


Comments
Yazid Wednesday, May 6, 2009
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


Scott Wednesday, May 6, 2009
@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!
suhair Thursday, May 7, 2009
Informative, Thanks
MikeS Sunday, May 10, 2009
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).
scott Sunday, May 10, 2009
@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.
Bryan Thursday, May 14, 2009
Scott, as nit-picky as it may be, they are write about the cast operation and you are wrong.
Scott Allen Thursday, May 14, 2009
@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 Ross Wednesday, November 11, 2009
Great article thanks. There isn't much info out there on how the model binders actually work.
mike Sunday, March 7, 2010
Very helpful, so little information on binders out there. Thanks for the write-up.
gravatar Amy Tuesday, August 3, 2010
Hey Scott,

Thanks for this great article. I've been going about developing with the MVC framework for the past year and I've only needed the default model binder until recently when I've been finding a weird twist on getting the Request.Files if your form has a file upload input control if the form is a partial view handled by Ajax. So in order to work around this, it now looks like I'll need to write an extension. Your article and the really insightful comments from everyone provides a lot of great points to consider while trying to navigate really sensitive pieces of the code. Thanks again. :)
gravatar scott Tuesday, August 3, 2010
@Amy: Always happy to help someone from a Pittsburgh email address.
gravatar Raj Thursday, September 30, 2010
Thanks for the informative article.

This use case is bugging me for a while. Anyone?

Here it is:
1. Recipe.Id is int.
2. User provides alpha-characters for 'Id' field.
3. I want to use TryParse/TryGet<int> but when it fails, I want to preserve what user-input was
4. I want to report back to user with user-input and error.

Any thoughts?
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!