Sunday, March 1, 2009

ASP.net MVC Model Binder

Model Binders is a powerful extension point to the ASP.NET MVC. It allows you to define your own class which is responsible for creating parameters for your controller actions. Scott Hansleman had a good example showing how to decouple a controller from HttpContext's user property.

Recently I've started a new Proof of Concept site which is based upon REST. My goal was to build a single web site which would expose its resources as addressable items. That is the URL defines the resource. This site should also be able to serve multiple clients which want different representations of the same resource.

For example assume we have an Automobile Parts catalog. We might have the browser point to http://myPartsCatalog.com/Ford/OilFilters/ which would render an HTML page of all the ford oil filters. Now let us say we want to implement an AJAX callback on the page so as a user mouse overs a specific part, the site might send a request to http://myPartsCatalog.com/Ford/OilFilters/fl1a, with the intention of gathering additional data about the part to display to the user. For this request we don't want to get an HTML representation of the part, we want the part rendered as JSON or XML, which will let us programatically access the part information.

I have grown tired of having to support multiple sites / pages one that renders the HTML and another that provides an API and exposes the data. The natural seperation of concerns that the MVC model gives us makes this an ideal platform to build a single web site which can serve HTML, XML, JSON or any other content we could imagine (for example what about a PDF version of the HTML page for offline usages).

To do this imagine we have the following controller defined:


public class PartController : Controller

{

public ActionResult List(string oem)

{

}

public ActionResult Detail(string oem, string partnumber)

{

}

}



We need a way to determine how the client wants the data to be returned. We could put that in the URL; however, that doesn't feel very restful. A key idea of a REST implementation is leveraging the capabilities in the HTTP protocol to define the resource your requesting. One of the Http Header's is Accept.

The Accept header allows us to define the content type that the client is requesting. So we could indicate in our request that we want text/json or text/xml. We could then put the following code in our controller:



        public ActionResult List(string oem)

        {

            //Get the Model for OEM.

            switch (HttpContext.Request.AcceptTypes[0])

            {

                case "*/*":

                case "text/html":

                    return View("PartList.aspx");

                case "text/json":

                    return View("PartList.json.aspx");

            }

        }



Warning: I am not finished investgating how ASP.Net handles the Accept header and my switch statement might not work; however, this is an example to highlight the flexibility of ASP.Net MVC.

So this works great; except that we want to unit test our code to ensure we are returning the right view for each request; however our controller is bound HttpContext which can be difficult to unit test. So the question is how do we decouple our controller from HttpContext? IModelBinder's are the answer.

We can define a ModelBinder by implementing the IModelBinder interface. The interface requires a single method that takes a couple parameters which provide context about the request, and returns an object.



    public class ContentTypeModelBinder : IModelBinder

    {

 

        #region IModelBinder Members

 

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

        {

            if (controllerContext == null)

            {

                throw new ArgumentNullException("controllerContext");

            }

            if (bindingContext == null)

            {

                throw new ArgumentNullException("bindingContext");

            }

 

            return controllerContext.HttpContext.Request.AcceptTypes[0];

        }

 

        #endregion

    }




The return value from the function will be used to pass in to a parameter in a controller. This allows us to change our controller definition to:

        public ActionResult List(string oem, [ModelBinder(typeof(ContentTypeModelBinder))]string contentType)

        {

            //Get the Model for OEM.

            switch (contentType)

            {

                case "*/*":

                case "text/html":

                    return View("PartList.aspx");

                case "text/json":

                    return View("PartList.json.aspx");

            }

        }



The magic here is the ModelBinder attribute which is applied to the contentType parameter. This tells the ASP.Net MVC runtime to use our own custom IModelBinder to provide the value for the contentType attribute.



The thing I don't like about this is that we have to use an attribtue in our controller. It is possible to indicate that all instances of a specific type use a specific binder. But I will leave that for a different article.

Enjoy

No comments: