Los Techies : Blogs about software and anything tech!

A better Model Binder


One of the more interesting extension points in ASP.NET MVC are the Model Binders.  Model Binders are tasked with transforming the HTTP Form and Querystring information and coercing real .NET types out of them.  A normal POST is merely a set of string key-value pairs, which isn’t that fun to work with.

Back in the ASP 3.0 days, where I cut my teeth, we did a lot of “Request.Form(“CustFirstName”)” action, and just dealing with the mapping from HTTP to strong types manually.  That wasn’t very fun.

ASP.NET MVC supplies the DefaultModelBinder, which is able to translate HTTP request information into complex models, including nested types and arrays.  It does this through a naming convention, which is supported at both the HTML generation (HTML helpers) and the consumption side (model binders).

Reconstituting complex model objects works great, especially in form posting scenarios.  But there are some scenarios where we want more complex binding, and for these scenarios, we can supply custom model binders.

Quick glance at custom model binders

ASP.NET MVC allows us to override both the default model binder, as well as add custom model binders, through the ModelBinders static class:

ModelBinders.Binders.DefaultBinder = new SomeCustomDefaultBinder();
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder());

The custom binders are bound by destination type.  We can also use the Bind attribute to supply the type of the custom binder directly on our complex model type.  The resolution code (from CodePlex) is:

private IModelBinder GetBinder(Type modelType, IModelBinder fallbackBinder)
{
    // Try to look up a binder for this type. We use this order of precedence:
    // 1. Binder registered in the global table
    // 2. Binder attribute defined on the type
    // 3. Supplied fallback binder

    IModelBinder binder;
    if (_innerDictionary.TryGetValue(modelType, out binder))
    {
        return binder;
    }

    binder = ModelBinders.GetBinderFromAttributes(modelType,
        () => String.Format(CultureInfo.CurrentUICulture, MvcResources.ModelBinderDictionary_MultipleAttributes, modelType.FullName));

    return binder ?? fallbackBinder;
}

Very nice, they even commented the order of precedence!

This is all well and good, but the custom model binding resolution leaves me wanting.  It only works if the destination type matches exactly with a type registered in the custom binder collection.  That’s great, unless you want to use something like binding against a base class, such as a layer super type of Entity or similar.  Suppose I want to bind all Entities by fetching them out of a Repository automatically?  That would allow me to just put our entity types as action parameter types, instead of GUIDs littered all over the place.  It gets even worse as some ORMs (NHibernate being one) use proxies to do lazy loading, and the runtime type is some crazy derived type you’ve never heard of.

But we can do better.

A new default binder

Instead of using the ModelBinders.Binders.Add method, let’s put all of our matching in our default binder, a new binder, a SmartBinder.  This SmartBinder can still have all the default binding logic, but this time, it will attempt to match individual custom binders themselves.  To do this, let’s define a new IModelBinder interface:

public interface IFilteredModelBinder : IModelBinder
{
    bool IsMatch(Type modelType);
}

Instead of our ModelBinderDictionary containing the matching logic, let’s put this where I believe it belongs – with each individual binder.  Model binders make a ton of assumptions about what the model information posted looks like, it makes sense that it’s their decision on whether or not they can handle the model type trying to be bound.  We created a new IFilteredModelBinder type, inheriting from IModelBinder, and added an IsMatch method that individual binders can use to match up their type.

The new SmartBinder becomes very simple:

public class SmartBinder : DefaultModelBinder
{
    private readonly IFilteredModelBinder[] _filteredModelBinders;

    public SmartBinder(IFilteredModelBinder[] filteredModelBinders)
    {
        _filteredModelBinders = filteredModelBinders;
    }

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        foreach (var filteredModelBinder in _filteredModelBinders)
        {
            if (filteredModelBinder.IsMatch(bindingContext.ModelType))
            {
                return filteredModelBinder.BindModel(controllerContext, bindingContext);
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }            
}

Our new SmartBinder depends on an array of IFilteredModelBinders.  This array is configured by our IoC container of choice (StructureMap in my case), so our SmartBinder doesn’t need to concern itself on how to find the right IFilteredModelBinders.

The BindModel method is overridden, and simply loops through the configured filtered model binders, asking each of them if they are a match to the model type attempting to be resolved.  If one is a match, its BindModel method is executed and the value returned.

If there are no matches, the logic defaults back to the built-in DefaultModelBinder progression, going through the commented steps laid out before.

I’ve found that our custom model binders can decide to bind a certain model for quite a variety of reasons, whether it’s a specific layer supertype and the types are assignable, or maybe the type is decorated with a certain attribute, it’s really up to each individual filtered model binder.

That’s the strength of this design – it puts the decision of what to bind into each individual model binder, providing as much flexibility as we need, instead of relying specifically on only one matching strategy, on concrete type.  Now testing custom model binders – that’s another story altogether.

Kick It on DotNetKicks.com
Posted Mar 17 2009, 10:07 PM by bogardj
Filed under:

Comments

DotNetShoutout wrote A better Model Binder - Jimmy Bogard
on 03-18-2009 1:08 AM

Thank you for submitting this cool story - Trackback from DotNetShoutout

Bolik wrote Interesting Finds: 2009-03-18
on 03-18-2009 11:13 AM

SOA Agents:当网格遇上SOA 在云中做一小时测试 为什么我们要放弃Subversion 如何使用VMDK to VHD Converter转换虚拟硬盘? 谈表达式树的缓存(3):使用前缀树 Unexpected

Fredrik Kalseth wrote re: A better Model Binder
on 03-19-2009 3:00 AM

That's much nicer. I totally agree that its better to have the binders decide whether they can handle some type or not (IsMatch). This of course redefines the order of precedence, so you have to think about the order in which you register the binders - or rather, the order in which the container injects them.

Reflective Perspective - Chris Alcock » The Morning Brew #310 wrote Reflective Perspective - Chris Alcock » The Morning Brew #310
on 03-19-2009 4:30 AM

Pingback from  Reflective Perspective - Chris Alcock  » The Morning Brew #310

Herding Code 43: Javier Lozano on the "M" in MVC | Herding Code wrote Herding Code 43: Javier Lozano on the "M" in MVC | Herding Code
on 04-17-2009 6:37 PM

Pingback from  Herding Code 43: Javier Lozano on the "M" in MVC | Herding Code

Faculty of The Mind wrote Constructor Injection for ASP.NET MVC Model Binders
on 04-18-2009 7:23 AM

Constructor Injection for ASP.NET MVC Model Binders

Jimmy Bogard wrote How we do MVC
on 04-24-2009 12:09 AM

Our team has been using the ASP.NET MVC framework on a production application for about 9 months now

K. Scott Allen wrote 6 Tips for ASP.NET MVC Model Binding
on 04-27-2009 12:16 AM

Model binding in the ASP.NET MVC framework is simple. Your action methods need data, and the incoming...

BusinessRx Reading List wrote 6 Tips for ASP.NET MVC Model Binding
on 04-27-2009 12:40 AM

Model binding in the ASP.NET MVC framework is simple. Your action methods need data, and the incoming

linkfeedr » Blog Archive » 6 Tips for ASP.NET MVC Model Binding - RSS Indexer (beta) wrote linkfeedr » Blog Archive » 6 Tips for ASP.NET MVC Model Binding - RSS Indexer (beta)
on 04-27-2009 1:24 AM

Pingback from  linkfeedr » Blog Archive » 6 Tips for ASP.NET MVC Model Binding - RSS Indexer (beta)

Jimmy Bogard wrote Refactoring Challenge Part 3 – Pattern Implementation
on 07-22-2009 9:21 PM

In the previous part to the refactoring challenge, I needed to structure the original implementation

lilikindsli wrote re: A better Model Binder
on 10-04-2009 7:47 PM

YcWT6r I want to say - thank you for this!

Aggregated specifications wrote Aggregated specifications
on 10-08-2009 7:07 AM

Pingback from  Aggregated specifications

Jimmy Bogard wrote A better Model Binder addendum
on 11-19-2009 9:31 PM

A while back, I wrote about a ModelBinder enhancement we use to do arbitrary filtering on types. 

Javier G. Lozano wrote MVC Turbine Redux
on 01-12-2010 12:34 AM

MVC Turbine Redux

Daniel Silva wrote re: A better Model Binder
on 01-13-2010 9:58 AM

How can a ModelBinder use my factory method, or standalone factory?

I´m getting confused when using ModelBinder's utility, but some properties have default initialization values required...

bogardj wrote re: A better Model Binder
on 01-17-2010 2:41 PM

@Daniel

Not sure I understand the question....can you explain a little more?

#.think.in wrote Modal Binding an Interface with DynamicProxy
on 02-16-2010 5:20 AM

Modal Binding an Interface with DynamicProxy

#.think.in wrote Modal Binding an Interface with DynamicProxy
on 02-17-2010 3:56 AM

Modal Binding an Interface with DynamicProxy

Dan wrote re: A better Model Binder
on 02-19-2010 2:57 PM

Could you possibly post the code/link for the StructureMap side of things?  I'm very new to it, but I am using now in a project and I'd like to see how you integrated it into your SmartBinder.

Thanks!

kitchaiyong wrote re: A better Model Binder
on 03-13-2010 3:19 PM

nice and cool.

DotNetKicks.com wrote A better Model Binder
on 03-13-2010 3:20 PM

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Add a Comment

(required)  
(optional)
(required)  
Remember Me?

Enter the numbers above:
Copyright Los Techies 2008, 2009. All rights reserved.
Powered by Community Server (Commercial Edition), by Telligent Systems