OdeToCode IC Logo

Recursive Model Binding

Thursday, July 15, 2010

In the last post we saw how a model binder has to show up for work even when all we need is a simple int parameter on an HTTP GET request.

Now it's time for a pop quiz!

Given this route definition:

routes.MapRoute(
    "SearchRoute",
    "Home/Browse/{category}",
    new {controller = "Home", action = "Browse"});

And this class definition:

public class BrowseRequest 
{
    public int StartYear { get; set; }
    public string Manufacturer { get; set; }
    public string Category { get; set; }
    public string Controller { get; set; }
    public string Action { get; set; } 
}

And given this request arrives as a GET:

http://server/home/browse/appliances?startyear=2009&manufacturer=GE

.. at this method of HomeController:

public ActionResult Browse(BrowseRequest search)
{
    // ...
    return View();
}

Then what will BrowseRequest look like?

Answer:

The DefaultModelBinder will create a new BrowseRequest object and populate it with the following values:

  • Action = "browse" (from route data)
  • Category = "appliances" (from route data)
  • Controller = "home" (from route data)
  • Manufacturer = "GE" (from the query string)
  • StartYear = 2009 (from the query string)

The default model binder is adept at building up entire objects using information it finds in the request environment. Binding removes all the clumsy code you would otherwise write to find and shuffle data around. The default model binder can even build up collections and complex object graphs

It's Recursive (Sort Of)

When the MVC runtime realizes it needs a BrowseRequest object, it looks for a model binder for the BrowseRequest type (a topic for a future post) and invokes the binder's BindModel method (it's part of the IModelBinder contract that every model binder must implement).

If the binder in question is the DefaultModelBinder, then it doesn't just jump in and start setting properties in one operation. The default model binder's brain works something like this*:

Default Model Binder: "Hmm, this looks like a complex thing with lots of properties. I'm going to loop through each property and tell some other model binder to do this work for me. Let's start with this Manufacturer property - it's of type string. Hey! MVC! Give me the model binder for string types!"

MVC Runtime: "Here is the model binder. Thank you for asking, please come again." 

Default Model Binder: "I will setup a new binding context for this binder and call BindModel --"

     Default Model Binder: "Hey - I'm binding a string named Manufacturer. Found it! Done! Pop!"

Default Model Binder: " -- and now that manufacturer is done, let me look at this StartYear of type int. Hey! MVC! Give me the model binder for int types!"

MVC Runtime: "Here is the model binder. Thank you for asking, please come again."

And this conversation continues until the binder visits all the properties. When you ask the model binder to bind a simple primitive type like string and int - it will do so, but anything more complex** and the binder effectively breaks up the big job into a series of smaller jobs. It turns out that most of the time the default model binder is recursively calling itself to do these smaller binding jobs, but the flow doesn't have to run this way.

Why Is This Important?

If you want to plug in a custom model binder this recursive behavior is important to understand. You can verify that binding a BrowseRequest object takes 6 calls to BindModel using a debugger and the following code:

public class MyModelBinder : DefaultModelBinder
{
    public override object BindModel(
        ControllerContext controllerContext, 
        ModelBindingContext bindingContext)
    {
        // set a break point inside this method
        return base.BindModel(controllerContext, bindingContext);
    }
}

And put the following into Application_Start:

ModelBinders.Binders.DefaultBinder = new MyModelBinder();

However, the default model binder doesn't know it's calling itself. It's just trying to delegate work to whatever model binder is registered for a given type. If you register a model binder for typeof(string) or typeof(int), the default model binder in this scenario would be calling into those other model binders.

In a future post we'll exploit this delegating behavior to simplify a binding scenario.

* There are a few simplifications made in this depiction. It's the essence that counts.

** There are always special cases - collections are an example of an exception to the this rule.