Saturday, January 7, 2012

ASP.NET MVC 3 controller action method parameter binding

When I started to play around with ASP.NET MVC one of the areas I needed more light to be shed on was controller action parameter binding. At the first glance the binding behaviour looked like "you name it - you get it" which seemed kind of suspicious. As appeared later there was not so little truth in the rule mentioned earlier.

In a very basic manner there are two classes involved in executing a controller action method and binding parameters. ControllerActionInvoker is a class responsible for finding an appropriate controller class action method by matching its signature to a route data (obviously it is not a single task performed by this class). Usually developers would not need to deal with this class directly.

After the appropriate action method is found a DefaultModelBinder class comes into play by managing method parameter binding. And this is were the magic begins.

Parameter values are bound using the data that arrives within a http request. The following request data is used when looking up data to be bound (in the order defined below):

  1. Posted form data
  2. Route data
  3. Querystring data
  4. Submitted file data

Keeping it short for simple action parameter types - action parameters matching request data keys are bound to corresponding request data values. All non matching action parameters are set their default values.

Keeping it short for complex action parameter types - action parameter type public properties matching request data keys are bound to corresponding request data values. All not matching properties are set their default values. Complex type action parameters are always instantiated.

I guess some examples would be a good place to start explaining the rules mentioned above.

A simple untyped view to display a list of product categories would call action like this:

public ActionResult Categories()
{
    ViewData["Categories"] = ProductNH.GetProductCategories();
    return View(); 
}

A corresponding view markup:

@using (Html.BeginForm("Categories", "Product", FormMethod.Get))
{ 
    <ul>
    @foreach (var category in (IList<Category>)ViewData["Categories"])
    {
        <li>@category.Name - @category.Descn</li>
    }
    </ul>
    
    <input type="submit" value="Refresh categories" />
}

Most probably you would notice the html form and the submit button being redundant in this particular situation and you would be absolutely right. But I would like to keep them as it is to stay in sync with my later samples.

Let's add some filtering functionality and update the action method to accept a filter parameter:

public ActionResult Categories(string namePart)
{
    ViewData["Categories"] = ProductNH.GetProductCategories(namePart);
    return View(); 
}

An updated view markup:

@using (Html.BeginForm("Categories", "Product", FormMethod.Get))
{ 
    <label for="namepart">Category name part: </label>
    <input type="text" name="namepart" />
    
    <ul>
    @foreach (var category in (IList<Category>)ViewData["Categories"])
    {
        <li>@category.Name - @category.Descn</li>
    }
    </ul>
    
    <input type="submit" value="Refresh categories" />
}

After the html form is submitted the "namepart" value comes to the server as a query string value (notice the form submit mode FormMethod.Get). It is then matched to the action parameter name (case-insensitive match is performed).

Let's change the form submit mode to FormMethod.Post. In such case the following actions are valid to serve a view request:

[HttpPost]
public ActionResult Categories(string namePart)
{
    ViewData["Categories"] = ProductNH.GetProductCategories(namePart);
    return View();
}

[HttpPost]
public ActionResult Categories(FormCollection formCollection)
{
    ViewData["Categories"] = ProductNH.GetProductCategories(formCollection["namepart"]);
    return View();
}

Next, move a little bit further: let's define a class for a category filter:

public class CategoryFilter
{
    public string NamePart { get; set; }
}

And let's pass it into the controller action:

[HttpPost]
public ActionResult Categories(CategoryFilter filter)
{
    ViewData["Categories"] = ProductNH.GetProductCategories(filter.NamePart);
    return View();
}

What happens here is the CategoryFilter class instance is constructed using default parameterless constructor and property NamePart is mapped to a posted form data value identified by the key "namepart". As you might have already noticed introducing the CategoryFilter class did not require any changes in the view markup. Lovely, isn't it?

Let's make the view to be a typed view and create a model class for it:

public class CategoryListModel
{
    public CategoryFilter Filter { get; set; }

    public IList<Category> Categories { get; set; }
}

In this case changes are necessary for the view markup as well:

@using (Html.BeginForm("Categories", "Product", FormMethod.Post))
{ 
    <label for="filter.namepart">Category name part: </label>
    <input type="text" name="filter.namepart" />
    
    <ul>
    @foreach (var category in Model.Categories)
    {
        <li>@category.Name - @category.Descn</li>
    }
    </ul>
    
    <input type="submit" value="Refresh categories" />
}

Notice the input name changes - it has a "filter" prefix before "namepart". And now the action:

[HttpPost]
public ActionResult Categories(CategoryListModel model)
{
    model.Categories = ProductNH.GetProductCategories(model.Filter.NamePart);
    return View(model);
}

What happens here is the CategoryListModel class instance is constructed using default parameterless constructor. Filter property value is instantiated the same way and its property NamePart is mapped to a prefixed posted form data value. Basically the prefix "filter" is mapped to a property name Filter. The rest of the binding is performed as described in the previous example.

So: you name it - you get it, isn't it so?

3 comments: