ASP.NET MVC4 Binding to a List of Complex Types

UPDATE: I have an idea how they might be doing it: Expression Trees!

One of my colleagues and I ran into a little trouble with Model Binding in ASP.NET MVC 4 yesterday. We wanted a form to post back an IEnumberable of a complex type, let’s call it IEnumberable. Doing this naively didn’t work. After a bit of googling, and experimentation we found out how to get what we wanted, but the answer was a bit quirky: you must use an array/collection index operator inside your Html.Whatever() lambda, otherwise the Html helper doesn’t know that it should put an array index at the front of the field names in your form. If it doesn’t put this index (in the form of “[0].”, “[1].”), then the MVC model binder can’t work out how to group your fields into instances of a complex type.

Our initial view looked something like this:


@model IEnumerable<ComplexItem>

@{
    ViewBag.Title = "ComplexList";
}

@using (Html.BeginForm())
{
    <h2>ComplexList</h2>
    foreach(var item in Model)
    {
        @Html.HiddenFor(m => item.Id)
        @Html.TextBoxFor(m => item.Title)
        @Html.TextBoxFor(m => item.Price)
    }
    <button type="submit">Submit</button>
}

And the action method it was posting back to looked something like:


[HttpPost]
public ActionResult Index(IEnumerable model)
{
    return View(model);
}

I went away to a meeting and came back to find that the problem had been solved, based on this link. Essentially, you can induce MVC to spit out a prefix on the field names (in the form of “[0].”, “[1].”, etc), which allows the model binder to group the fields together into a collection of complex types. Now, I still felt like we had unfinished business because I wanted to know how it works, so I conducted an experiment.

Referencing Phil Haack’s example, my example becomes something like this:


@model IList<ComplexItem>

@{
    ViewBag.Title = "ComplexList";
}

@using (Html.BeginForm())
{
    <h2>ComplexList</h2>
    for (int i = 0; i < 0; Model.Count; i++) 
    { 
        @Html.HiddenFor(m => Model[i].Id)
        @Html.TextBoxFor(m => Model[i].Title)
        @Html.TextBoxFor(m => Model[i].Price)
    }
    <button type="submit">Submit</button>
}

And I also changed the action method to use IList instead of IEnumberable. This works, but I was trying to determine how it was emitting those prefixes (or at least what was causing it).

So, enter the scientific method:

Hypothesis 1: The collection must be a type that has an “IndexOf” or “FindIndex” method (hence Arrays and IList<T> working, and IEnumerable<T> not working).

To try this I went down the foreach route (ie the first example), but with IList<ComplexType>. No joy. To further test this I used an array as well, still no joy. Hypothesis 1 disproved.

Hypothesis 2: It is the act of calling the [] operator inside the lambda.

So, working backwards from the working code, I normalized out the “Model[i]” bit:


@model IList<ComplexItem>

@{
    ViewBag.Title = "ComplexList";
}

@using (Html.BeginForm())
{
    <h2>ComplexList</h2>
    for (int i = 0; i &lt; Model.Count; i++) 
    { 
        var item = Mode[i];        
        @Html.HiddenFor(m => item.Id)
        @Html.TextBoxFor(m => item.Title)
        @Html.TextBoxFor(m => item.Price)
    }
    <button type="submit">Submit</button>
}

This did not work, which leads me to conclude that it has to do with the act of calling the [] operator in the lambda. Hypothesis 2 confirmed, for now.

I still don’t know how it works, but at least I know exactly what to do to get it to work. This also leaves me in awe of the power of lambdas, and the ability to break a lambda down into an expression tree, and deal with the result in wildly different ways, based on the form of the expression.

UPDATE: I’ve found a feature of Lambdas that would facilitate the behavior we’re seeing here, Expression Trees. You’ll notice that the parameter for Html.TextBoxFor() etc is Expression<Func<…>>. By wrapping the lambda in an expression object, you get the ability to break it down into its components. When you do that you can detect things like whether there is an array index happening etc. I’ll show you what I mean below:


using System;
using System.Linq.Expressions;

namespace ExpressionTrees
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Program();
            var data = new string[] { "one", "two", "three" };

            for (int i = 0; i < data.Length; i++)
            {
                p.Investigate(() => data[i].ToLower());
            }
        }

        public void Investigate(Expression<Action> exp)
        {
            Console.WriteLine(exp.CanReduce);
        }
    } 
}

Stick a breakpoint on the “Console.WriteLine” line in Visual Studio, and then take a look at the magic!

The debug string for my expression
The debug string for my expression

As you can see, there’s an object, and it knows that there’s a closure, of which the “data” object is a member, and another closure, of which the “i” array index is a member, and it seems to know the sequence of operations. Let’s dig further:

Inside the Expression
Inside the Expression

We can see that at the top level we’ve got a NodeType of Lambda, which has a Body. That body is a Call of Method on an Object. Looking at the object:

How you detect an Array Access
How you detect an Array Access

We can see that the Object is an ArrayIndex. You’ve got a Left which is the array ((ExpressionTrees.Program+<>c__DisplayClass0).data), and you’ve got a Right which is the index ((ExpressionTrees.Program+<>c__DisplayClass2).i).

Short of decompiling, or finding the source for Html.TextBoxFor, I would think they are analyzing the expression, much like this, and selecting a codepath to handle the result. The graphics geek in me has his propeller hat spinning really fast right now, because you could do some really interesting stuff with this for scene querying.

6 thoughts on “ASP.NET MVC4 Binding to a List of Complex Types

  1. Can’t make it work with VB.NET using @Html.TextBoxFor(Function(m) m.Visitors(index).Name) sets the name attribute to “Name” not to “Name[index]”.. Any sugestion? 😦

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s