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.

28 Comments

  • Thanks a lot Scott.

    I'm interested in seeing your code. Please email me your solution to nmarun at gmail dot com. If you have DB dependencies, see if you can modify your app to read data from an XML file.

    Arun

  • Sent as requested, Arun. Thanks for taking a look. Any help is appreciated!

    Scott

  • Scott, I've not received it.. even checked the spam folder. If you have Windows Live account, use skydrive and upload it there and send me the link.

    Arun

  • No doubt, the post is really fantastic and very useful, for every one who is new in MVC. I were also facing the same problem for last few days and implemented the same solution up to large extent but could not get success till now, now after reading this will try to apply exactly whatever is said in this solution tomorrow.
    It would be better It this post also tell how to add new product to existing list at runtime in mvc, instead of just editing the existing product

  • Good to know this article helped you understand Model Binding in MVC.

    Adding products is pretty much similar to the edit. In the post version of the Create action method, you need to get an instance of the new product that needs to be added. All you need to do then, is to persist it to a DB and then read the new list of products from the DB to show it to the user.

    Let me know if you're having an issue there.

    Arun

  • Scott, you're right. I'm not sure why your code did not bind the model correctly. You can test this using the source code. Steve's blog describes how you can do this:
    http://blog.stevensanderson.com/2009/02/03/using-the-aspnet-mvc-source-code-to-debug-your-app/

    Arun

  • Nice post! &nbsp;I thought I would have to roll my own, but the naming conventions here saved me from doing so!

    Cheers,

    Jon

  • Glad you found it useful Jon.

    Arun

  • I downloaded the zip file of the code, but I get an error uncompressing the file saying it's invalid or corrupted.

  • Dean,

    Turns out you need to just click on the link in the blog post above. This'll take you to the skydrive page and there you click on the download button.

    Arun

  • Thank you so much Arun. It worked perfectly. You've saved quite a lot of my time.
    Let me waste it on Reddit now :)

  • Excellent Post ...

  • Very nice article.. it helped me a lot.. Tnx

  • Glad you found it useful Vasanth.

    Arun

  • ooops,

    My previous post is still in moderation (maybe the moderator can just change it ;-) but there is a bug in that function I posted (i++ should be count++)

    as in:

    function UpdateSubClassNames() {
    $('[class^="subClass"]').each(function () {
    var classes = this.className.split(' ');
    var model;
    for (var i = 0, len = classes.length; i < len; i++) {

    if (classes[i].substring(0, 8) == 'subClass') {
    model = classes[i].split(":")[1];
    }
    }
    if (model != null) {
    var isFirst = true;
    var count = 0;
    $(this).find('tr').each(function () {
    //skip header row
    if (isFirst) {
    isFirst = false;
    } else {
    $(this).find(':input').each(function () {
    var oldName = this.name;
    this.name = model + "["+count+"]."+oldName;
    });
    count++;
    }
    });
    }
    });
    }

  • @@gordatron - I think there's been some mix-up with your post. My post doesn't talk anything about JavaScript. I'll go ahead and remove your comment.

    Arun

  • yeah feel free to delete, I messed up the posting by submitting a version that didn't work and the one that is now showing does not work and also does not explain what it does ;-)

    Its an example of getting javascript to add the IDs and prefix the names of the inputs correctly. To be honest you need to add another method for re-ordering them when you remove one and so its really probably not all that useful without a wider explanation and some other code anyway.

  • Nice stuff... Really helped me a lot.
    thank u

  • @@Ed, glad it helped you.

    Arun

  • Great article. However, it doesn't seem to work when I use Html.Encode instead of Html.Textbox. Any idea why?

  • @Foon Lam, If you're talking about replacing Html.Textbox with Html.Encode in line 20 (say) of the second snippet, it won't work because Html.Encode will only render a read-only label on the page which will not get posted back to the server. Hope that answers your question.

    Arun

  • Nice post. Thanks!

    I downloaded the code and it works. But, I'm trying to do something similar for my project but it doesn't work. I don't get an updated object(Basket) in the controller Index function after posting the page.

    Something that I'm missing.

    Please let me know!

  • Nice Post. Thanks!
    I don't understand why it doesn't working in my project.
    I don't get updated object of Basket(in the controller) after posting the page.
    Please advise.

  • Hi Manoj, glad you found the post useful. I think you're better of posting your question in one of the many forums to get a quicker reply.

    Arun

  • Great post. After a lot of hours i bumped into this tutorial and it solved my problem!

    The only thing I've noticed is that for:

    &lt;%= Html.TextBox(string.Format("Products[{0}].ID", i), Model.Products[i].Id) %&gt;

    my string format needs to be equal to the "path" of the model, so building a string for my project I had to use

    "("Basket.Products[{0}].ID", i)" instead of only Products, kind of weird...but it works!

    Thanks for sharing

    Gyo

  • This was exactly what I was looking for. Thanks for spending the time to post this, really.

    -Kelly.

  • Excellent post! You've saved me a lot of time!

  • Absolutely what I was looking for thanks awesome post

Comments have been disabled for this content.