Wednesday, September 09, 2009

The most important thing nobody told you about model binders

A bold statement, but I stand by it.

I have done numerous searches on how to implement custom model binders in ASP.NET MVC, and all of them are variations of:

public override object GetValue(ControllerContext ctx, string modelName, Type modelType, ModelStateDictionary state)
{
   Customer customer = new Customer();
   customer.FirstName = ctx.HttpContext.Request["FirstName"];
   customer.LastName = ctx.HttpContext.Request["LastName"];
   // ... other properties ...
   return customer;
}


But that’s counter productive, isn’t it? ASP.NET MVC is all about conventions and flexibility. The last thing I want from my model binders are to hardcode expected property names, which would fail anyway. If my type is ReportIdentificator, I certainly don’t want to give all my properties and parameters the very same name. I want my freedom to call them foo and Bar, if that’s what I need.



So I finally went to the source, so to speak. The ASP.NET MVC source code is available, and inside of the DefaultModelBinder, I found the following gem:



    if (!performedFallback) {
        ValueProviderResult vpResult;
        bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out vpResult);
        if (vpResult != null) {
            return BindSimpleModel(controllerContext, bindingContext, vpResult);
        }
    }


The getaway here is bindingContext.ModelName. That property is the key to get the proper ValueProviderResult, which contains the posted values for the model. With that information, I am finally able to create the model binder I want.



The usual binder registration:



    ModelBinders.Binders.Add(typeof (ReportIdentifier), new ReportIdentifierBinder());


And my binder:



    public class ReportIdentifierBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var model = (ReportSectionIdentifier)base.BindModel(controllerContext, bindingContext);
            ValueProviderResult serialized;
            if (model == null && bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out serialized))
            {
                var values = serialized.AttemptedValue.Split("-".ToCharArray());
                model = new ReportSectionIdentifier
                            {
                                Organization = int.Parse(values[0]),
                                Period = int.Parse(values[1]),
                                Report = int.Parse(values[2]),
                            };
            }
            return model;
        }
    }


I let the base try to create the model first, in those cases where the usual conventions are followed. My implementation deserialize the model from a value on the form “1-2-3”. That code enables me to support drop down lists bound to a report name and its identifier, which posts a value on the form “1-2-3”, or use the identifier in action links, which appends the property values to the querystring according to the conventions.

No comments: