Top Tips For ASP.NET MVC Model Binding

August 22nd, 2016

For those new to model binding, there are a number of conventions to make it work properly. Today, we look at a couple of tips and gotchas when using model binders.

"How do you perform a postback in ASP.NET MVC?"

This is a question a lot of people ask coming from a WebForms background.

I'm sorry to say that it doesn't exist.

However, the MVC world is a little bit different than the WebForms world. You can use any number of ways of passing data back to the controller and vice-versa.

First, let's focus on what model binding is before we dig into it.

The Basics

Model Binding is merely the process of taking your data from your view on a POST, "binding" it into a usable object (a model), and passing it into a controller.

Consider this an abstracted step from the WebForms world where you hit a submit button and immediately have access to the form's data through a variable.

By default, there is a DefaultModelBinder (source) already in place for posting back data. Most people use the default model binder for their needs, but if you absolutely need to write one, there are some things you need to know.

A while ago, I wrote a simple model binder for query strings for sending paging data into a controller making it a little easier to work with instead of a FormCollection. 

To implement a custom model binding, you attach a ModelBinder attribute to your model and create a new ModelBinder to handle the conversion of data into an object. This example is taken from the model binder above with query strings.

Models/PagingModel.cs

using System.Web.Mvc;
using ModelBindingExample.ModelBinders;
 
namespace ModelBindingExample.Models
{
    [ModelBinder(typeof(PagingBinder))]
    public class PagingModel
    {
        public int PageIndex { get; set; }
        public int PageSize { get; set; }
    }
}

ModelBinder\PagingBinder.cs

using System.Web.Mvc;
using ModelBindingExample.Models;
 
namespace ModelBindingExample.ModelBinders
{
    public class PagingBinder: DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext,
            ModelBindingContext bindingContext)
        {
            var request = controllerContext.HttpContext.Request;

            var index = request.QueryString.Get("Page");             var size = request.QueryString.Get("Size");
            int pageIndex;             if (!int.TryParse(index, out pageIndex))                 pageIndex = 0; // Default Page
            int pageSize;             if (!int.TryParse(size, out pageSize))                 pageSize = 20; // Default 20 FAQs

            return new PagingModel             {                 PageIndex = pageIndex,                 PageSize = pageSize             };         }     } }

Optionally, if you don't want to attach an attribute to your model, you can easily add one to your global.asax instead.

System.Web.Mvc.ModelBinders.Binders.Add(typeof(PagingModel), new PagingBinder());

Now that you understand the basics, we can move forward with some gotchas.

Arrays/Lists

One project I'm currently working on requires a large amount of data to be sent through a view model. This includes a wide number of object types in the view model.

When you pass these view models over to the view, there are some conventions required to make these objects model bind properly.

When working with large, deep objects, you sometimes miss some of the obvious types when serialization happens when a new object is created.

For example, if you have a type in your object defined as

public Attraction[] Attractions { get; set; }

you may experience an error when the model tries to bind to it. It tries to "Clear()" out the enumerable because it thinks it's of type List<>, but it can't clear it out because it's an array and Clear() isn't a method on arrays.

Modify the above line instead to read

public List<Attraction> Attractions { get; set; }

and your model binding will work as expected.

Bind to a Model, not a Variable

As you write your Views, you may experience some interesting side effects to your model binding.

For example, I'm using a list of Disney attractions that I want to edit on the screen and I place all of these attractions on the screen.

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    foreach (var attraction in Model.DisneyAttractions.Attractions)
    {
        var location = attraction.Location;
        @Html.TextBoxFor(model => location.LocationName)
    }
}

There are a couple problems with this approach.

Do you think model binding will occur for this for..each loop?

No, it won't.

Why?

Because it's binding to a variable and not the model.

If you look at the rendered code, you'll notice that it looks a little funny.

<form action="/" method="post">
    <input id="location_LocationName" name="location.LocationName" type="text" value="">
    <input id="location_LocationName" name="location.LocationName" type="text" value="">
    <input id="location_LocationName" name="location.LocationName" type="text" value="">
</form>

When you post the form, the model binder can't make heads or tails as to where this data is meant to reside.

A better way of writing this code would be

@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    for (int index = 0; index < Model.DisneyAttractions.Attractions.Count; index++)
    {
        @Html.TextBoxFor(model => Model.DisneyAttractions.Attractions[index].Location.LocationName)
    }
}

Our rendered HTML result looks like this

<form action="/" method="post">
    <input id="DisneyAttractions_Attractions_0__Location_LocationName" name="DisneyAttractions.Attractions[0].Location.LocationName" type="text" value="">
    <input id="DisneyAttractions_Attractions_1__Location_LocationName" name="DisneyAttractions.Attractions[1].Location.LocationName" type="text" value="">
    <input id="DisneyAttractions_Attractions_2__Location_LocationName" name="DisneyAttractions.Attractions[2].Location.LocationName" type="text" value="">
</form>

Now, I know what you're thinking.

"Why use a for..next loop? Why not keep the for..each?"

The reason is because we need to keep track of where our lists of objects reside.

It may seem a little wordy with the for..next, but I guarantee you, it will read the names of the fields and create new instances of the objects...all...the...way...down...the...tree. You will have a complete object for this huge ViewModel.

The reason this works is because you are leaving clues for the Model Binder. You have provided a huge breadcrumb trail in the name. When it reads the name, it matches up your ViewModel data from the Forms, rebuilds the ViewModel, and passes it on the controller.

This part is important: If you don't have any corresponding form data on the View when you post, the model binder will build the object and look for the data from the form. If it's not on your form, those properties in your ViewModel will be null.

If you want your data to persist, you need a Html.HiddenFor() in your View so it can attach data to the ViewModel when it model binds.

At one point, I had a webgrid in a form and was wondering why the data wasn't returning with the ViewModel.

Yeah, I know...one of those dumb moments. It was text on the screen in a table, not actual form data returned. I needed a way to pass that data back to a populated ViewModel, so I used Html.HiddenFor() for each grid row.

Just wanted to make this tip clear to everyone.

I also meant this as a reminder to myself for when I'm old and senile.

Don't Use Generic Controls

Unless you are performing some type of custom model binding, I would recommend against using the Html.TextBox(), Html.DropDownList(), and others.

These controls require you to enter a control name. Creating a name for your control that matches the path of the property would be extremely hard to write. Just look at the name attribute on the example above.

As mentioned above, it would be better to use the xxxxxFor() controls instead (Html.TextBoxFor(), Html.DropDownListFor(), etc.)

When bound to a model, these controls provide the assistance needed to bring back your ViewModel from the dead and allow it to be sent down to the controller on postback.

Conclusion

Model Binding provides the basics of how to consolidate your form data and send it off to a controller in a neat little package.

While the DefaultModelBinder gives the developer 90% functionality out of the box, it also provides a hook to make your own custom ModelBinder in case you need a certain way of doing something, like uploading a file into a ViewModel.

Have any more questions about Model Binding? Did this answer your question? No? Post your comment below and let's discuss.