Auto Wire-Up Your Model Binders ASP.NET MVC

Page content

Model binding in MVC attempts to map values from IValueProviders to your specified model. The value providers (FormValueProvider, QueryStringValueProvider, HttpFileCollectionValueProvider, RouteDataValueProvider, JsonValueProviderFactory … ) abstract the actual value retrieval and binders then handle the value mapping onto the model.

You can create custom model binders to abstract and centralise complex mapping logic that would otherwise end up in your controllers or services. When dealing with a larger number of binders, it is good to refactor common logic and enable wiring up of the new binders easily. Instead of having to register each binder one-by-one in global or prefix the type in the actions, we can create our own model binder broker to replace the default MVC DefaultModelBinder.

A Common Scenario

Retrieving a persisted domain object from the database that is then passed to the ViewModel. Lets assume we have a database of books and we want to get a single book by it’s ID.

 1public ActionResult Book(Guid? id)
 2{
 3    if (!id.HasValue) 
 4        return HandleInvalidBook();
 5 
 6    var book = _repository.FindById<Book>(id);
 7    if (book == null)
 8        return HandleInvalidBook();
 9 
10    var model = new BookModel() { Book = book };
11    return View(model);
12}

Binder Code

Rather than doing all the lookup work in the controller, you can create a BookBinder that will call the repo and retrieve the Book object by given ID. The BinderBase expects the type of the domain object and the type of the ID. It auto-converts the value from IValueProviders into the expected ID type and calls BindModelWithId.

 1public class BookBinder : BinderBase<Book, Guid>
 2{
 3    private readonly IRepository _repository;
 4 
 5    public BookBinder(IRepository repository)
 6    {
 7        _repository = repository;
 8    }
 9 
10    protected override object BindModelWithId(ControllerContext controllerContext, ModelBindingContext bindingContext, Guid id)
11    {
12        return _repository.FindById<Book>(id);
13    }
14}
15 
16public abstract class BinderBase<T, TId> : IFilteredBinder
17{
18    protected virtual string ValueIdentifier
19    {
20        get { return "id"; }
21    }
22 
23    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
24    {
25        var itemId = GetObjectId(controllerContext, bindingContext);
26        return BindModelWithId(controllerContext, bindingContext, itemId);
27    }
28 
29    protected virtual TId GetObjectId(ControllerContext controllerContext, ModelBindingContext bindingContext)
30    {
31        var value = bindingContext.ValueProvider.GetValue(ValueIdentifier);
32        return value == null ? default(TId) : Convert<TId>(value.AttemptedValue);
33    }
34 
35    protected abstract object BindModelWithId(ControllerContext controllerContext, ModelBindingContext bindingContext, TId id);
36 
37    public Type ShouldBind
38    {
39        get { return typeof (T); }
40    }
41 
42    private static TId Convert<TId>(string input)
43    {
44        try
45        {
46            var converter = TypeDescriptor.GetConverter(typeof(TId));
47            if (converter != null)
48            {
49                return (TId) converter.ConvertFromString(input);
50            }
51            return default(TId);
52        }
53        catch
54        {
55            return default(TId);
56        }
57    }
58}
59 
60public interface IFilteredBinder : IModelBinder
61{
62    Type ShouldBind { get; }
63}

Using Model Binders To Retrieve The Book Object

You can then use your binder on your Controller Action

1public ActionResult Book(Book book)
2{
3    if (book == null)
4        return HandleInvalidBook();
5 
6    var model = new BookModel() {Book = book};
7    return View(model);
8}

A Smart Way To Wire-Up Your Model Binders

Registering binders one-by-one in the Global.asax or prefixing them in the Action gets tedious and it’s easy to make a mistake. A smarter way is to implement a default model binder and decide which binder to use based on ShouldBind property of each IFilteredBinder.

 1public class ModelBinderBroker : DefaultModelBinder
 2{
 3    private readonly IEnumerable<IFilteredBinder> _binders;
 4 
 5    public ModelBinderBroker(IEnumerable<IFilteredBinder> binders)
 6    {
 7        _binders = binders;
 8    }
 9 
10    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
11    {
12        if(_binders == null || _binders.Count() == 0)
13            return base.BindModel(controllerContext, bindingContext);
14 
15        var binder = _binders.FirstOrDefault(x => x.ShouldBind == bindingContext.ModelType);
16        return binder == null ? base.BindModel(controllerContext, bindingContext) : binder.BindModel(controllerContext, bindingContext);
17    }
18}

Register Your Binder Broker In Global.asax

You can then set your custom ModelBinderBroker as the DefaultBinder.

 1protected void Application_Start()
 2{
 3    var container = new Castle.Windsor.WindsorContainer();
 4    container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel, true));<br>
 5    //binders
 6    container.Register(
 7        AllTypes.FromAssembly(typeof(IFilteredBinder).Assembly)
 8        .BasedOn<IFilteredBinder>()
 9    );
10    container.Register(Component.For<ModelBinderBroker>().ImplementedBy<ModelBinderBroker>());
11    container.Register(Component.For<IRepository>().ImplementedBy<FakeRepository>());
12 
13    RegisterBinders(container);
14}
15 
16private void RegisterBinders(IWindsorContainer container)
17{
18    ModelBinders.Binders.DefaultBinder = container.Resolve<ModelBinderBroker>();
19}

Download code

Phil Haack – What is the difference between a value provider and model binder http://haacked.com/archive/2011/06/30/whatrsquos-the-difference-between-a-value-provider-and-model-binder.aspx)

Mehdi Golchin – Deep Dive Into Value Providers http://mgolchin.net/posts/19/dive-deep-into-mvc-ivalueprovider