Use ModelBinder to Generically Bind Complex Types

[Update: Simone brought my attention to the fact that ComplexModelBinder which comes with the framework does exactly that.  You can find more info here]

ASP.NET MVC Preview 5 introduce the ModelBinder attribute that can be used to decorate a complex type in an Action.  This allows us to have actions that look like this

public ActionResult Create([ModelBinder(typeof(GenericBinder))] ContactList myList)

 

Instead of this:

public ActionResult Create(string name, string description)

 

The problem is that you have to build a Binder for every complex type you want to use as a parameter.  For example, Maarten Balliauw created a model binder on his blog and it looks like this:

public class ContactBinder : IModelBinder
{
    #region IModelBinder Members
    public object GetValue(ControllerContext controllerContext, 
                 string modelName, Type modelType, 
                 ModelStateDictionary modelState)
    {
        if (modelType == typeof(Contact))
        {
            return new Contact
            {
                Name = controllerContext.HttpContext.Request.Form["name"] ?? "",
                Email = controllerContext.HttpContext.Request.Form["email"] ?? "",
                Message = controllerContext.HttpContext.Request.Form["message"] ?? ""
            };
        }
        return null;
    }
    #endregion
}

 

Now that is a lot of typing and because I am lazy, I decided to create a generic binder that uses reflection and can work with all my complex types. 

Note: By generic I mean common – it has nothing to do with .net Generics

Also note that this will only work if you follow these conventions:

  1. The html field name must match the property name
  2. User lower case names for the html fields
  3. You don’t have to user lower case on your model properties

Here is the very rough and untested Generic Binder:

class GenericBinder : IModelBinder
{
    public object GetValue(ControllerContext controllerContext, 
                            string modelName, Type modelType, 
                            ModelStateDictionary modelState)
    {
        var instance = Activator.CreateInstance(modelType);
        foreach (var prop in modelType.GetProperties())
        {
            prop.SetValue(instance, 
                    controllerContext.HttpContext.Request
                                    .Form[prop.Name.ToLower()], 
                    null);
        }
        return instance;
    }
}

 

If you find any bugs or have a better implementation, please share.

  • http://codeclimber.net.nz Simone

    Isn't this what the ComplexBinder that comes with MVC P5 already does?

  • eibrahim

    You are absolutely right. It is hard to find these things without good
    documentation. But I just found some info at
    http://weblogs.asp.net/scottgu/archive/2008/09/
    I will update the post to direct readers there.

    Thanks for the heads up.

  • eibrahim

    You are absolutely right. It is hard to find these things without good
    documentation. But I just found some info at
    http://weblogs.asp.net/scottgu/archive/2008/09/
    I will update the post to direct readers there.

    Thanks for the heads up.

  • http://codeclimber.net.nz Simone

    No problem :)
    btw: this threaded comment view is awesome

  • eibrahim

    Yeah, I love Disqus… I actually don't have to log in to my admin tool to
    moderate my comments. I simply reply to a comment with “approve” and it
    gets approved. Or If I reply with some other text, it approves the comment
    and submits my text as a comment. Very neat.
    I am looking into the ComplexModelBinder and trying to get things working
    with the built-in stuff :)

  • Steve

    The ComplexModelBinder doesn't make much sense in his post.

    How do you use it? Why the need to 'register' it.

    Seems to me if you pass a complex type as a parameter, it would map the form variables to the type with reflection – and only if you need to have more complex mapping would you create a specific binder…

  • http://stephenedwards.virtualituk.com/ Stephen Edwards

    Well how about this:

    I exposed the protected UpdateModel in my controller:

    public void UpdateModel(BusinessSavingsRequest bsr, string[] keys)
    {
    base.UpdateModel(bsr, keys);
    }

    and then call it from my binder:

    public object GetValue(ControllerContext controllerContext, string modelName, Type modelType, ModelStateDictionary modelState)
    {
    BusinessSavingsRequest bsr = new BusinessSavingsRequest();
    string[] properties = typeof(BusinessSavingsRequest).GetProperties().Select(p => p.Name).ToArray();
    ((MvcApplication.Controllers.BusinessSavingsController)controllerContext.Controller).UpdateModel(bsr, properties);
    return bsr;
    }

    Perhaps if this is all it needed to do I wouldnt have a custom binder for the type.

  • http://stephenedwards.virtualituk.com/ Stephen Edwards

    Well how about this:

    I exposed the protected UpdateModel in my controller:

    public void UpdateModel(BusinessSavingsRequest bsr, string[] keys)
    {
    base.UpdateModel(bsr, keys);
    }

    and then call it from my binder:

    public object GetValue(ControllerContext controllerContext, string modelName, Type modelType, ModelStateDictionary modelState)
    {
    BusinessSavingsRequest bsr = new BusinessSavingsRequest();
    string[] properties = typeof(BusinessSavingsRequest).GetProperties().Select(p => p.Name).ToArray();
    ((MvcApplication.Controllers.BusinessSavingsController)controllerContext.Controller).UpdateModel(bsr, properties);
    return bsr;
    }

    Perhaps if this is all it needed to do I wouldnt have a custom binder for the type.