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:
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 an MVC action
- Also send back additional parameter(s) to the same MVC action
- Sending back read-only data
- By Ajax
Solution
When the user hits this site, the HomeController
’s Index
will be called:
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:
</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<dynamic>
’ 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 the appropriate type, here List<MvcApplication1.Models.FruitModel>
.
The partial view _Fruit
is:
</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:
</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 generates a post back collection as shown below when submitting the form.
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<FruitModel>
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 made in the above code are:
- Used
List<T>
for @model
instead of IEnumerable<T>
, hence I can use Count
property. - Used for
i = 0…List<T>.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
:
</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 sends 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 “
<add key=”UnobtrusiveJavaScriptEnabled” value=”true” />
” 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.
CodeProject