Henry Chong

MVC Model Binding: Complex Objects and Collections

A Person Assembling a Gingerbread House

📷: Leeloo The First

Also known as "Why is my ViewModel returning null values to my HttpPost action?"  The answer is at the very bottom if you feel like skipping straight to it.

Let's say for example you have the following ViewModel and classes:

public class MyViewModel
{
public ComplexObject MyComplexObject { get; set; }
public int Value { get; set; }
public IEnumerable<ComplexObject> ComplexCollection { get; set; }
}
public class ComplexObject
{
public int ID { get; set; }
public string Name { get; set; }
}

Attempt #1

If you had the following in your view:

@using (Html.BeginForm())
{
<input type="submit" value="Submit" />
}

Looking in your controller:

[HttpPost]
public ActionResult Submit(MyViewModel viewModel)
{
// some stuff here
return new ContentAction();
}

If you put a breakpoint and debug, you'll see that viewModel won't actually have any of the values you need. That's fine, we know how to handle that - we need to actually provide form input fields so that the data can be sent to the controller. We'll use Hidden fields, and everything will be fine; a standard HTML concept. Let's try and get our complex object back.

Attempt #2

@using (Html.BeginForm())
{
@Html.HiddenFor(m => Model.MyComplexObject)
<input type="submit" value="Submit" />
}

So we added in a hidden field for the complex object - but if you breakpoint and debug, it'll still be null. What gives? It turns out you'll need to actually bind the individual properties of the object, or the default model binder will just ignore you.

Attempt #3

@using (Html.BeginForm())
{
@Html.HiddenFor(m => Model.MyComplexObject.Name)
<input type="submit" value="Submit" />
}

Excellent! Now we have the Name property when we debug (ID will still be unbound, though). What about our list? Let's try and bind that…

Attempt #4

@using (Html.BeginForm())
{
@Html.HiddenFor(m => Model.MyComplexObject.Name)
@Html.HiddenFor(m => Model.ComplexCollection)
<input type="submit" value="Submit" />
}

No good - but we kind of knew that would happen anyway - the model binder doesn't magic up entire objects for us, so it's not too surprising it won't do a collection either. After a trawl of the internet to figure out why, the answer to binding a collection is that you need to explicitly provide an ID, as well as the properties you want bound. The simplest way to do this is to change the IEnumerable<> to an IList<> (there are other ways to do this if you don't want to use IList<>), and then for each item in your collection, create the hidden inputs.

Attempt #5

@using (Html.BeginForm())
{
@Html.HiddenFor(m => Model.MyComplexObject.Name)
for (var i = 0; i < Model.ComplexCollection.Count; i++)
{
@Html.HiddenFor(x => x.ComplexCollection[i].ID)
@Html.HiddenFor(x => x.ComplexCollection[i].Name)
}
<input type="submit" value="Submit" />
}

Now if you debug, you should see the values you expect! Congratulations!

The answer then, in summary:

You must explicitly create a hidden input for each property in your complex object that you want to be bound - being lazy and attempting to create a hidden input for the entire object directly won't work (unless you create a Template for it - which is probably also better practice…)

IEnumerables and binding don't play very nicely directly out of the box - it looks like MVC has better base support for IList<> and arrays, but you'll still have to enumerate the collection and create hidden inputs for each item.

Good luck out there!