Custom Model Binders

Learn what you can do when the UI doesn’t match your data model

Many developers sometimes complain about ASP.NET MVC's productivity, which they believe is inferior to that guaranteed by ASP.NET Web Forms. The point is debatable but, in general, it's not a blasphemy to state that server controls make building the user interface of a Web Forms applications quick compared to building the same user interface in ASP.NET MVC. This said, ASP.NET MVC does take productivity into account, introducing a number of features that don’t exist in Web Forms but that speed up common development tasks significantly. An extremely cool productivity boost is model binding.

 

Last month, I covered the basics of model binding and showed how the framework manages to bind posted data to parameters in the signature of controller methods. In this article, I'll go one step further and discuss what you can do to create custom model binders to personalize the way in which specific pieces of data in the user interface will be exposed to controllers.

 

Why Model Binders?

As mentioned, model binders don't exist in ASP.NET Web Forms. Or, more exactly, they don't exist in the same form they take in ASP.NET MVC. In Web Forms, after a postback each instance of an existing server control is updated with any significant posted data. This behavior is hardcoded in the page life cycle and developers just take advantage of it. In a way, server controls represent the data model you work with in Web Forms. In ASP.NET MVC, controllers are responsible for implementing the expected behavior of a request. Model binding just lets you expose posted data to controllers in an easy-to-use way.

 

The net effect of model binding is loading any fields found in the body of the HTTP request into a public member of a target object or into an argument in the signature of a controller method.

 

The DefaultModelBinder class takes care of this task and uses a lot of reflection to match input fields to data members. The basic rule employed by DefaultModelBinder is name matching. As long as the name of a posted field matches the name of a member, the member will take the posted value. This forces you to use fixed names in the HTML forms and helper classes you use between the controller and the view.

The DefaultModelBinder class offers a bit of flexibility through the Bind attribute, but can’t do much when the layout of the data being posted by the user interface doesn’t match perfectly the layout you would like to work with in the backend. On the other hand, as an architect you should consider altering the user interface experience to obtain data that fits more easily to the server-side data model. When the gap between posted data and the server-side data model can’t be bridged using the native customization capabilities of DefaultModelBinder, then all you do is create one or more custom binders.

 

Custom Model Binders

To introduce custom model binders, let me start with a canonical example. Imagine that your user interface contains multiple input fields for users to enter content. For example, let's say you're creating an input form to accept a date. Suppose also that you decide to employ distinct input fields for day, month, and year.

 

However, the same content scattered through multiple input fields in the user interface will be processed as a single property by the server-side business logic. The default model binder doesn’t know how to put the distinct pieces (day, month, year) together; only the developer knows about that. Subsequently, the developer must provide an application-specific component that contains only the logic to process distinct chunks of data into a single object. Figure 1 shows the role of the extra logic you might need that the default binder can’t provide.

 

A custom model binder receives any posted data and makes itself responsible for picking up all the pieces it needs to prepare and return a specific data object. Technically speaking, a custom model binder is a class that implements the IModelBinder interface, as shown below:

 

public interface IModelBinder

\\{

    object BindModel(

        ControllerContext controllerContext, ModelBindingContext bindingContext);

\\}

 

As you can see, the BindModel method is simply expected to return an object built using any data and information being posted. Note that for some reason, in ASP.NET MVC 2 the interface is not generic. But I believe a IModelBinder<T> type would be a nice thing to have. Let’s delve deeper into the BindModel method.

 

The method receives two parameters—the controller context and the model binding context. The former is a system class that wraps together various pieces of information about the ongoing HTTP request. It provides access to the HTTP context (and ASP.NET intrinsics), request, and route data. The ModelBindingContext object is another container class that gathers pieces of information the binder can find useful. Among other things, the binding context includes the value provider (abstraction for form, query string, route data) and the model state to add information about the state of the object being modeled.

 

Note that when writing a model binder getting information for the model from the posted data is only the most common scenario. It's not the only possibility you have; in fact, you can grab information from anywhere. For example, you can read data from ASP.NET cache and session state, parse, and store it into the model.

 

A Sample Custom Binder

Figure 2 shows the source code of a sample model binder that returns a DateTime object. The code for the BindModel method first gets (or creates) an instance of the model object (in this case, the DateTime type) and then populates its members using any custom processing it needs on posted data. To access posted data, you normally use a helper method that just uses the value provider in the binding context to extract data.

 

If the posted data is not a valid format you can throw an exception or store errors into the returned model. You can also choose a default value. For example, the default model binder resets to zero any input in a field that attempts to bind to an integer member. At the same time, you can also call the AddModelError method on the ModelState property of the binding context object to mark the resulting model object as invalid. This is useful if you're then using the model API to check values on the server before starting an operation. You probably won’t do that for a simple type like DateTime; however, you might want to do that if you're moving posted data to a more complex type, as demonstrated below:

 

public ActionResult PerformTask(UpdateViewModel model)
\\{
    // Is the provided model object valid?
    if (!model.IsValid)
    \\{
      :

    \\}
\\}

 

Registering a Model Binder

So far you’ve seen how to create a custom model binder to populate a specific type from posted data. Once created, however, a model binder must be registered to become usable. You can associate a model binder with its target type in either of two ways: globally or locally.

 

A global association is performed only once, usually at the start of the application, and entails that any occurrence of model binding for the type will be resolved through the registered binder. If you go for a local association instead, you apply the binding to individual occurrences of parameters in a controller method. Global association takes place in the global.asax file. Here’s some sample code:

 

void Application_Start()

\\{

    :

    ModelBinders.Binders\\[typeof(CustomerViewModelBinder)\\] =

                         new CustomerViewModelBinder();

\\}

 

You create a new entry in the system dictionary of binder objects exposed through the Binders property. The entry in the dictionary is based on a key represented by the type. As you can see from the preceding code within Application_Start, you can have multiple binders registered. Local association is attribute-based and requires the following syntax:

 

public ActionResult Edit(

           \\[ModelBinder(typeof(CustomerViewModelBinder))\\]                       

           CustomerViewModel customerInfo)

\\{

   :

\\}

 

You use the ModelBinder attribute and pass it the type of the custom binder class you created. Finally, note that if both local and global binders apply to a given type, then global binders take precedence over local binders.

 

Tweaking the Default Binder

There are situations in which you don’t really need to create a custom model binder from scratch. Sometimes, you can achieve the same results with a class derived from the default binder. In doing so, you won’t lose any of the built-in behavior and you can modify the default behavior to achieve any additional behavior you might need.

 

For example, suppose that all you want is to validate the work of the default binder against a specific type. In this case, you can override the method OnModelUpdated on the DefaultModelBinder class and insert your own validation logic, as appropriate. Figure 3 shows what you can do.

 

The OnModelUpdated method provides a centralized console where you control and validate all properties in the model. You can choose to override OnModelUpdated, if you feel comfortable keeping all validations in a single place. Otherwise, you can override the OnPropertyValidating method, if you prefer to validate properties individually. Once you've arranged your own version of the standard binder, you have the problem of replacing it with the ASP.NET MVC runtime. You use the following code to replace the default binder:

 

ModelBinders.Binders.DefaultBinder = new MyNewDefaultBinder();

 

As a final note, consider that ASP.NET MVC 2 supports a second binder component that represents the first alternative to the default model binder—the data annotations model binder class. You register it as follows:

 

// Place this code in Application_Stat inside global.asax

ModelBinder.Binders.DefaultBinder = new DataAnnotationsModelBinder();

 

The major benefit of the data annotation model binder is that you get a free evaluation of the annotations existing on the class every time you attempt to do some model binding.

 

The End Result

In ASP.NET MVC, controller methods typically receive through their signature any data they need to work with. This means that some code is required that maps plain input data posted from HTML forms to classes and types used in the controller methods’ signatures. 

 

Typically, the controller is responsible for such a mapping layer of code but, if placed there, the controller's code would be bloated. The model binder is a class that contains such a mapping layer and removes some boilerplate code from the controller. In doing so, the model binder makes the entire ASP.NET MVC quite an elegant (and effective) framework. The default binder works most of the time; if not, you can create custom binders for all types, or for a specific type, quite easily.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish