I once met a bright young person from a mysterious place where I think dragons might live. He asked: “Hey Camilo, when I attempt to bind my view model to the controller, I get a null
.” In this article, I would like to take a tour through the DefaultModelBinder
in ASP.NET MVC.
Turns out, there is a ton of amazing capability you get for free right out of the box.
Models
Before I begin, I would like to take a moment to talk about the models we’ll bind to in C#. Let’s begin with a Person
that has a Friend
and Addresses
.
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Address
{
public int Id { get; set; }
public string City { get; set; }
public string State { get; set; }
}
public class PersonViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public Person Friend { get; set; }
public Address[] Addresses { get; set; }
}
This is a bit contrived since I hope a person
has more than one friend
. Since I hail from America, I’ve decided to stick with states.
You can think of Person
and Address
as tables in a database. I prefer to put models that get strongly typed to the view in view models. I like to think of view models as aggregates that sum up data.
This is what the controller
looks like:
public class PersonController : Controller
{
public ActionResult Edit()
{
var vm = new PersonViewModel
{
Id = 1,
Name = "Jane Doe",
Friend = new Person
{
Id = 2,
Name = "Jon Doe"
},
Addresses = new Address[]
{
new Address
{
Id = 1,
City = "Athens",
State = "Texas"
},
new Address
{
Id = 2,
City = "Paris",
State = "Texas"
}
}
};
return View(vm);
}
[HttpPost]
public ActionResult Edit(PersonViewModel vm)
{
return View("Details", vm);
}
}
You might be wondering if Athens and Paris actually exist in Texas. Well they do, and are nice places to see. No dragon sightings there, I’m afraid.
You can begin to imagine how the default binder is going to handle the entire view model. I’ve written minimal code to deal with a wide array of data.
Binding to Complex Types
You can think of the model binder as the machine that churns an HTTP text message into a C# object.
To bind complex C# types, all we need in the Razor view is this:
@using (Html.BeginForm("Edit", "Person"))
{
<div>
@Html.HiddenFor(m => m.Id)
<label>Name:</label>
<span>@Html.TextBoxFor(m => m.Name)</span>
</div>
<div>
<button>Submit</button>
</div>
}
The HTML rendered is super simple:
<form action="/" method="post">
<div>
<input id="Id" name="Id" type="hidden" value="1" />
<label>Name:</label>
<span>
<input id="Name" name="Name" type="text" value="Jane Doe" />
</span>
</div>
<div>
<button>Submit</button>
</div>
</form>
The default binder takes the name
attribute from the input
tag to bind C# property objects. The value
attribute sets the value. This is what the HTTP request body looks like when I click “Submit”:
Id=1&Name=Jane+Doe
I’d like you to think of HTTP requests as a key / value pairs. So, name
points to the property name in C# and value
to its value. It behaves the same as doing this in your controller:
var vm = new PersonViewModel();
vm.Id = 1;
vm.Name = "Jane Doe";
What is amazing is how the default binder takes the strong typing in C# into account. String
types get string
values, and integer types get integer values.
With me so far? Let’s go in a bit deeper.
Binding to Nested Types
Let’s pick up the pace a bit and move on to a nested type. All we need is:
<div>
<label>Friend:</label>
@Html.HiddenFor(m => m.Friend.Id)
<span>@Html.TextBoxFor(m => m.Friend.Name)</span>
</div>
It looks awfully complicated. But, all the default binder sees is Friend.Name
. So, it recurses through the C# objects to find the property. The entire engine is recursive in nature, so you can go as deep as you like. This will support complex datasets right out of the box.
If you go back to the controller, you’ll see PersonViewModel vm
as the method parameter in Edit()
. The default model binder sees this object graph when it binds the view model:
new PersonViewModel
{
Friend = new Person
{
Id = 2,
Name = "Jon Doe"
}
};
It picks all that up from this HTML:
<div>
<label>Friend:</label>
<input id="Friend_Id" name="Friend.Id" type="hidden" value="2" />
<span>
<input id="Friend_Name"
name="Friend.Name"
type="text"
value="Jon Doe" />
</span>
</div>
As long as you map name
attributes to the correct object property, the default model binder will handle the rest.
List Binding
The default binder needs a way to keep track of a list of objects. By convention, it uses an array-like syntax to do this. Let’s have a look:
for (var i = 0; i < Model.Addresses.Count(); i++)
{
<div>
@Html.HiddenFor(m => m.Addresses[i].Id)
<label>City:</label>
<span>@Html.TextBoxFor(m => m.Addresses[i].City)</span>
<label>State:</label>
<span>@Html.TextBoxFor(m => m.Addresses[i].State)</span>
</div>
}
And this is the HTML:
<div>
<input id="Addresses_0__Id"
name="Addresses[0].Id"
type="hidden" value="1" />
<label>City:</label>
<span>
<input id="Addresses_0__City"
name="Addresses[0].City"
type="text"
value="Athens" />
</span>
<label>State:</label>
<span>
<input id="Addresses_0__State"
name="Addresses[0].State"
type="text"
value="Texas" />
</span>
</div>
<div>
<input id="Addresses_1__Id"
name="Addresses[1].Id"
type="hidden" value="2" />
<label>City:</label>
<span>
<input id="Addresses_1__City"
name="Addresses[1].City"
type="text"
value="Paris" />
</span>
<label>State:</label>
<span>
<input id="Addresses_1__State"
name="Addresses[1].State"
type="text"
value="Texas" />
</span>
</div>
Personally, I am not in favor of the wacky syntax. You may have noticed, if you go back to the view model, that I use an Addresses[]
array to make the magic work. The default model binder gets to be picky about indexes. So, as long as you zero-index and increment consecutively, it will work. Just make sure there are no gaps in your indexes. There is sort of a hack to get around index gaps, but I don't recommend it since this is already weird enough.
Conclusion
It’s time for the grand finale! Let’s see the default model binder in action:
And to geek out, here is the HTTP request body:
Id=1&Name=Jane+Doe&Friend.Id=2&Friend.Name=Jon+Doe&Addresses%5B0%5D.Id=1&
Addresses%5B0%5D.City=Athens&Addresses%5B0%5D.State=Texas&
Addresses%5B1%5D.Id=2&Addresses%5B1%5D.City=Paris&Addresses%5B1%5D.State=Texas
You may now go back to the original C# object I created in the controller to make sense of that blob of text. Feel free to admire the beauty of the default model binder. Yes, it is okay to shed a few tears.
If interested, you may find the entire demo up on GitHub.
The post The DefaultModelBinder in ASP.NET MVC for N00bs appeared first on BeautifulCoder.NET.