Collection Binding in ASP.NET MVC3 with AJAX

| Comments

There is a less-common scenario in web applications where we need to edit collection of objects and submit the whole back to the system. For example, let us take the below view model:

1
2
3
4
5
public class FruitModel...
        public string Name { get; set; }
        public bool IsFresh { get; set; }
        public bool IsPacked { get; set; }
      public decimal UnitPrice { get; set; }

The UI for this scenario is shown below:

Leave the top and bottom “Lorem ipsum” text, these are just gap fillers.  The user can change the “IsFresh” and “IsPacked” settings of the fruits and the unit prices.

Challenge

This post addresses the following simple problems when using ASP.NET MVC3:

  • Sending back collection of data to a MVC action
  • Also send back additional parameter(s) to the same MVC action
  • Sending back read-only data
  • By Ajax

Solution

When the user hitting this site, the HomeController’s Index will be called:
1
2
3
4
5
6
7
8
9
10
public ActionResult Index()...
  List<FruitModel> collection = new List<FruitModel>()
  {
      new FruitModel {Name = "Apple", IsFresh=true, IsPacked=false, UnitPrice = 10M},
      new FruitModel {Name = "Orange", IsFresh=false, IsPacked=false, UnitPrice = 5M},
      new FruitModel {Name = "Strawberry", IsFresh=true, IsPacked=true, UnitPrice = 15M}
  };
  ViewBag.NetAmount = IncludeTax(collection.Sum(fm => fm.UnitPrice));
  ViewBag.ShopId = Guid.NewGuid();
  return View(collection);
In the Index view, I’ve used NetAmount value of ViewBag as shown below:
1
2
3
4
5
6
7
8
9
10
11
</div>
<div>
<pre><h2>Welcome to Fruit Shop</h2>
<div>Lorem ipsum... </div>
<div>
  @Html.Partial("_Fruit", (List<MvcApplication1.Models.FruitModel>)Model)
</div>
<div id="netAmountDiv" name="netAmountDiv" style="color:Blue">
  Net Amount: @ViewBag.NetAmount
</div>
<div>Lorem ipsum...</div>
The main part of the Fruit Shop is defined in _Fruit partial view.  It requires the FruitModel collection and shop ID (in ViewBag).

Simply passing the Model in @Html.Partial(…) will throw the error “‘System.Web.Mvc.HtmlHelper’ has no applicable method named ‘Partial’ but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.”.  So, cast it to appropriate type, here List<MvcApplication1.Models.FruitModel>.

The partial view _Fruit is
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
</div>
<div>
<pre>@model List<MvcApplication1.Models.FruitModel>

@using (Ajax.BeginForm(new AjaxOptions
        {
            HttpMethod = "Post",
            UpdateTargetId = "netAmountDiv"
        }
))
{

<table>
    <tr>
        <th>
            Name
        </th>
        <th>
            IsFresh
        </th>
        <th>
            IsPacked
        </th>
        <th>Unit Price</th>
    </tr>

@for (int i = 0; i < Model.Count; i++)
{
    <tr>
        <td>
            @Html.DisplayFor(modelItem => Model[i].Name)
            @Html.HiddenFor(modelItem => Model[i].Name)
        </td>
        <td>
            @Html.EditorFor(modelItem => Model[i].IsFresh)
        </td>
        <td>
            @Html.EditorFor(modelItem => Model[i].IsPacked)
        </td>
        <td>
            @Html.EditorFor(modelItem => Model[i].UnitPrice)
        </td>
    </tr>
}

</table>
        <input type="hidden" id="shopId" name="shopId" value="@ViewBag.ShopId" />
        <div>
          <input name="submitFruit" type="submit" value="Change" />
      </div>
}
Now the important point here is, when you want to post back collection of FruitModel, the naming pattern of every HTML item in the collection should be “obj-name[index].property-name”.  For example, for the above code, ASP.NET generates HTML for an item like below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</div>
<div>
<pre><td>
  Apple
  <input name="[0].Name" type="hidden" value="Apple" />
</td>
<td>
  <input checked="checked" class="check-box" data-val="true" data-val-required="The IsFresh field is required." name="[0].IsFresh" type="checkbox" value="true" /><input name="[0].IsFresh" type="hidden" value="false" />
</td>
<td>
  <input class="check-box" data-val="true" data-val-required="The IsPacked field is required." name="[0].IsPacked" type="checkbox" value="true" /><input name="[0].IsPacked" type="hidden" value="false" />
</td>
<td>
  <input class="text-box single-line" data-val="true" data-val-number="The field UnitPrice must be a number." data-val-required="The UnitPrice field is required." name="[0].UnitPrice" type="text" value="10.00" />
</td></pre>
</div>
<div>
This HTML code actually generate a post back collection as shown below when submitting the form.
1
2
3
4
submitFruit=Change&[0].Name=Apple&[0].IsFresh=true&[0].IsFresh=false&[0].IsPacked=false&
[0].UnitPrice=10.00&[1].Name=Orange&[1].IsFresh=false&[1].IsPacked=true&[1].IsPacked=false&
[1].UnitPrice=5.00&[2].Name=Strawberry&[2].IsFresh=true&[2].IsFresh=false&[2].IsPacked=true&
[2].IsPacked=false&[2].UnitPrice=25&shopId=c9517c6b-c911-4a28-9a0a-3e47ccb60bd8&X-Requested-With=XMLHttpRequest
The above data matched with List model and with the other parameter name too.  The additional parameter I’m passing is “shopId” hidden value which is received from ViewBag.ShopId.  The main changes I did in the above code are:
  • Used List for @model instead of IEnumerable, hence I can use Count property.
  • Used for i = 0…List.Count instead of foreach.
ASP.NET MVC3 uses “name.propertyname” pattern, if you use “foreach”.  This wouldn’t send back the collection to the server.  Now, let us see the Index action for POST:
1
2
3
4
5
6
7
8
9
</div>
<div>
<pre>[HttpPost]
public ActionResult Index(Guid shopId, List<FruitModel> collection)...
  decimal addlTax = 0M;
  if (collection.Any(fm => fm.UnitPrice > 200)) addlTax += 2M;
  return Content("Net Amount: " + IncludeTax(collection.Sum(fm => fm.UnitPrice) + addlTax).ToString());</pre>
</div>
<div>
Leave the tax calculation stuff, it is just for making some difference from GET Index().  The above method send back the tax calculation as plain text to the client.  This is the place for AJAX.  This can be achieved by Ajax.BeginForm() in the above code, where I’ve mentioned that the result should be placed on an element with id “netAmountDiv”.  So, we can get the result asynchronously.  To make this AJAX.BeginForm() to work, you have to:
  • include jQuery’s unobtrusive AJAX script (jquery.unobtrusive-AJAX.min.js)
  • add “” option in appsetting section of web.config

Also, note that to send read-only item as part of the collection, in the above example FruitModel.Name, use hidden input control also.