ASP.NET MVC 2 Model Binding for a Collection
Yes, my yet another post on Model Binding (previous one is here), but this one uses features presented in MVC 2.
How I got to writing this blog? Well, I’m on a project where we’re doing some MVC things for a shopping cart. Let me show you what I was working with. Below are my model classes:
1: public class Product
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5: public int Quantity { get; set; }
6: public decimal UnitPrice { get; set; }
7: }
8:
9: public class Totals
10: {
11: public decimal SubTotal { get; set; }
12: public decimal Tax { get; set; }
13: public decimal Total { get; set; }
14: }
15:
16: public class Basket
17: {
18: public List<Product> Products { get; set; }
19: public Totals Totals { get; set;}
20: }
The view looks as below:
1: <h2>Shopping Cart</h2>
2:
3: <% using(Html.BeginForm()) { %>
4:
5: <h3>Products</h3>
6: <% for (int i = 0; i < Model.Products.Count; i++)
7: { %>
8: <div style="width: 100px;float:left;">Id</div>
9: <div style="width: 100px;float:left;">
10: <%= Html.TextBox("ID", Model.Products[i].Id) %>
11: </div>
12: <div style="clear:both;"></div>
13: <div style="width: 100px;float:left;">Name</div>
14: <div style="width: 100px;float:left;">
15: <%= Html.TextBox("Name", Model.Products[i].Name) %>
16: </div>
17: <div style="clear:both;"></div>
18: <div style="width: 100px;float:left;">Quantity</div>
19: <div style="width: 100px;float:left;">
20: <%= Html.TextBox("Quantity", Model.Products[i].Quantity)%>
21: </div>
22: <div style="clear:both;"></div>
23: <div style="width: 100px;float:left;">Unit Price</div>
24: <div style="width: 100px;float:left;">
25: <%= Html.TextBox("UnitPrice", Model.Products[i].UnitPrice)%>
26: </div>
27: <div style="clear:both;"><hr /></div>
28: <% } %>
29:
30: <h3>Totals</h3>
31: <div style="width: 100px;float:left;">Sub Total</div>
32: <div style="width: 100px;float:left;">
33: <%= Html.TextBox("SubTotal", Model.Totals.SubTotal)%>
34: </div>
35: <div style="clear:both;"></div>
36: <div style="width: 100px;float:left;">Tax</div>
37: <div style="width: 100px;float:left;">
38: <%= Html.TextBox("Tax", Model.Totals.Tax)%>
39: </div>
40: <div style="clear:both;"></div>
41: <div style="width: 100px;float:left;">Total</div>
42: <div style="width: 100px;float:left;">
43: <%= Html.TextBox("Total", Model.Totals.Total)%>
44: </div>
45: <div style="clear:both;"></div>
46: <p />
47: <input type="submit" name="Submit" value="Submit" />
48: <% } %>
Nothing fancy, just a bunch of div’s containing textboxes and a submit button. Just make note that the textboxes have the same name as the property they are going to display. Yea, yea, I know. I’m displaying unit price as a textbox instead of a label, but that’s beside the point (and trust me, this will not be how it’ll look on the production site!!).
The way my controller works is that initially two dummy products are added to the basked object and the Totals are calculated based on what products were added in what quantities and their respective unit price. So when the page loads in edit mode, where the user can change the quantity and hit the submit button. In the ‘post’ version of the action method, the Totals get recalculated and the new total will be displayed on the screen. Here’s the code:
1: public ActionResult Index()
2: {
3: Product product1 = new Product
4: {
5: Id = 1,
6: Name = "Product 1",
7: Quantity = 2,
8: UnitPrice = 200m
9: };
10:
11: Product product2 = new Product
12: {
13: Id = 2,
14: Name = "Product 2",
15: Quantity = 1,
16: UnitPrice = 150m
17: };
18:
19: List<Product> products = new List<Product> { product1, product2 };
20:
21: Basket basket = new Basket
22: {
23: Products = products,
24: Totals = ComputeTotals(products)
25: };
26: return View(basket);
27: }
28:
29: [HttpPost]
30: public ActionResult Index(Basket basket)
31: {
32: basket.Totals = ComputeTotals(basket.Products);
33: return View(basket);
34: }
That’s that. Now I run the app, I see two products with the totals section below them. I look at the view source and I see that the input controls have the right ID, the right name and the right value as well.
1: <input id="ID" name="ID" type="text" value="1" />
2: <input id="Name" name="Name" type="text" value="Product 1" />
3: ...
4: <input id="ID" name="ID" type="text" value="2" />
5: <input id="Name" name="Name" type="text" value="Product 2" />
So just as a regular user would do, I change the quantity value of one of the products and hit the submit button. The ‘post’ version of the Index method gets called and I had put a break-point on line 32 in the above snippet. When I hovered my mouse on the ‘basked’ object, happily assuming that the object would be all bound and ready for use, I was surprised to see both basket.Products and basket.Totals were null. Huh?
A little research and I found out that the reason the DefaultModelBinder could not do its job is because of a naming mismatch on the input controls. What I mean is that when you have to bind to a custom .net type, you need more than just the property name. You need to pass a qualified name to the name property of the input control.
I modified my view and the emitted code looked as below:
1: <input id="Product_Name" name="Product.Name" type="text" value="Product 1" />
2: ...
3: <input id="Product_Name" name="Product.Name" type="text" value="Product 2" />
4: ...
5: <input id="Totals_SubTotal" name="Totals.SubTotal" type="text" value="550" />
Now, I update the quantity and hit the submit button and I see that the Totals object is populated, but the Products list is still null. Once again I went: ‘Hmm.. time for more research’. I found out that the way to do this is to provide the name as:
1: <%= Html.TextBox(string.Format("Products[{0}].ID", i), Model.Products[i].Id) %>
2: <!-- this will be rendered as -->
3: <input id="Products_0__ID" name="Products[0].ID" type="text" value="1" />
It was only now that I was able to see both the products and the totals being properly bound in the ‘post’ action method. Somehow, I feel this is kinda ‘clunky’ way of doing things. Seems like people at MS felt in a similar way and offered us a much cleaner way to solve this issue.
The simple solution is that instead of using a Textbox, we can either use a TextboxFor or an EditorFor helper method. This one directly spits out the name of the input property as ‘Products[0].ID and so on. Cool right? I totally fell for this and changed my UI to contain EditorFor helper method.
At this point, I ran the application, changed the quantity field and pressed the submit button. Of course my basket object parameter in my action method was correctly bound after these changes. I let the app complete the rest of the lines in the action method. When the page finally rendered, I did see that the quantity was changed to what I entered before the post. But, wait a minute, the totals section did not reflect the changes and showed the old values.
My status: COMPLETELY PUZZLED! Just to recap, this is what my ‘post’ Index method looked like:
1: [HttpPost]
2: public ActionResult Index(Basket basket)
3: {
4: basket.Totals = ComputeTotals(basket.Products);
5: return View(basket);
6: }
A careful debug confirmed that the basked.Products[0].Quantity showed the updated value and the ComputeTotals() method also returns the correct totals. But still when I passed this basket object, it ended up showing the old totals values only. I began playing a bit with the code and my first guess was that the input controls got their values from the ModelState object.
For those who don’t know, the ModelState is a temporary storage area that ASP.NET MVC uses to retain incoming attempted values plus binding and validation errors. Also, the fact that input controls populate the values using data taken from:
- Previously attempted values recorded in the ModelState["name"].Value.AttemptedValue
- Explicitly provided value (<%= Html.TextBox("name", "Some value") %>)
- ViewData, by calling ViewData.Eval("name") FYI: ViewData dictionary takes precedence over ViewData's Model properties – read more here.
These two indicators led to my guess. It took me quite some time, but finally I hit this post where Brad brilliantly explains why this is the preferred behavior. My guess was right and I, accordingly modified my code to reflect the following way:
1: [HttpPost]
2: public ActionResult Index(Basket basket)
3: {
4: // read the following posts to see why the ModelState
5: // needs to be cleared before passing it the view
6: // http://forums.asp.net/t/1535846.aspx
7: // http://forums.asp.net/p/1527149/3687407.aspx
8: if (ModelState.IsValid)
9: {
10: ModelState.Clear();
11: }
12:
13: basket.Totals = ComputeTotals(basket.Products);
14: return View(basket);
15: }
What this does is that in the case where your ModelState IS valid, it clears the dictionary. This enables the values to be read from the model directly and not from the ModelState.
So the verdict is this: If you need to pass other parameters (like html attributes and the like) to your input control, use
1: <%= Html.TextBox(string.Format("Products[{0}].ID", i), Model.Products[i].Id) %>
Since, in EditorFor, there is no direct and simple way of passing this information to the input control. If you don’t have to pass any such ‘extra’ piece of information to the control, then go the EditorFor way.
The code used in the post can be found here.