I am using an Ajax.BeginForm inside a For Loop with allows me to post to the controller for each item in a the for loop. Initially the for loop renders each item correctly on page load. When the controller has handled the incoming View Model which is populated correctly from the Ajax.BeginForm, the method sends back a newly generated view model back to the View, however each item in my For loop is now duplicated, and all the textboxes now have the value of the submitted View Model.
Very confused how to structure this code correctly, I need the submission to work based on ajax and partial view. I did look at using JQuery to submit, but was concerned I may lose the AntiForgeryToken and that the Ajax.BeginForm was a more elegant approach. Any help greatly appreciated. Happy to provide more information also.
VIEW
@model Namespace.Models.MyParentViewModel
@for (int i = 0; i < Model.Items.Count; i++)
{
using (Ajax.BeginForm("SaveItem", "Controller", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "pensions" }))
{
@Html.AntiForgeryToken()
@Html.ValidationSummary()
<div>
@Html.Hidden("ID", Model.Items[i].ID)
@Html.TextBox("TheName", Model.Items[i].TheName, new { @class = "form-control", @id = "item-" + i})
<input type="submit" value="Save" class="btn save" name="Command" />
</div>
}
}
CONTROLLER
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("SaveItem")]
public ActionResult SaveItem(MyItemViewModel mivm, string Command)
{
if (ModelState.IsValid)
{
#do some logic
}
// Return a newly populated MyViewModel with updated mivm.
var model = PopulateMyParentViewModel();
return PartialView("_MyPartialView", model);
}
private MyParentViewModel PopulateMyParentViewModel()
{
List<MyItemsViewModel> lstItems = new List<MyItemsViewModel>();
foreach (var item in enity.items.OrderBy(p => p.ID).ToList())
{
var ExistingItem = new MyItemViewModel
{
ID = item.ID,
TheName = item.TheName
};
lstItems.Add(MyItemViewModel);
}
MyParentViewModel.Items = lstItems;
return MyParentViewModel
}
I am not sure what is "pensions" in your code, I guess it is a div surrounding every form generated by the loop? If so your problem is the following:
First, if none of the items are interrelated (that is, triggering some update on an item causes another item to be updated as well), you shouldn't waste resources to send the whole list back to the browser - consider just sending back the updated display of the affected "record" and just update the corresponding item with the response.
What causes items to be duplicated: the AjaxOptions
class has a property InsertionMode
, and as I can recall, its default value is InsertionMode = InsertionMode.InsertAfter
, which causes the duplicates to appear. Why? You send an Ajax request to the server, which sends back an HTML fragment populated by the entire list, instead of just one item, then the browser appends that fragment to the existing records.
Solution
If you remake your project to just send back one record instead of all of them, the just add an uniquely identified (= has an unique id
attribute) <div>
element around the using(...)
block, set InsertionMode
to InsertionMode=InsertionMode.Replace
and set the UpdateTargetId
property to the id
of that <div>
, like so:
<div id="record-@i">
@using (Ajax.BeginForm("SaveItem", "Controller", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "record-" + i.ToString(), InsertionMode = InsertionMode.Replace }))
{
// ...
}
</div>
That will cause the Ajax response to replace the contents of the container from which the request is sent (by contents I mean that the wrapping element itself is not replaced! You would need to use InsertionMode.ReplaceWith
for that behavior). If you mistake this, you end up dumping crazy amounts of nested elements inside the host container, possibly breaking scripts and/or styles which use very specific selectors.
If you stick to sending a whole collection of items back, then just wrap the for loop with a <div>
tag, give it an id
, set InsertionMode
to InsertionMode=InsertionMode.Replace
, and the UpdateTargetId
property to the id
of that <div>
.
--------- Update -----------
To answer your comment:
Assume you have a view which - for the sake of simplicity - displays records in a tabular format. Then, the result looks like this:
Name | Age | Salary | Hide
------------------------------------
Peter | 32 | $15k | Hide button
Eva | 28 | $12k | Hide button
Assume what you change with an Ajax request is that whenever you hide a record, you send back a single table row with a button that now displays 'show' instead of 'hide' and the same data about the person which belongs to the button that triggered the request. Then, when you make a direct (=non-ajax, when you directly navigate to the page) request, you do this:
@model PeopleCollection
...
<table>
<thead>
<tr>
<td>Name</td>
<td>Age</td>
<td>Salary</td>
<td>Hide</td>
</tr>
</thead>
<tbody>
@foreach (Person record in Model) {
@Html.Partial("TableRow_Partial",record)
}
</tbody>
The "TableRow_Partial" view looks like this:
@model Person
@{
AjaxOptions opts = new AjaxOptions() {
UpdateTargetId = "person-row-" + Model.Id.ToString(),
InsertionMode = InsertionMode.ReplaceWith
// etc
};
}
...
<tr id="person-row-@Model.Id">
<td>@Model.Name</td>
<td>@Model.Age</td>
<td>@Model.Salary</td>
<td>
<!-- configure the BeginForm(..) call with the action name and a routeValue anonymous object as "new { id = Model.Id}" -->
@using (Ajax.BeginForm(...)) {
<input type="submit" value="@(Model.IsHidden ? "Show" : "Hide")" />
}
</td>
</tr>
This will simply extract the construction of loops into a separated partial view. The action method procession your Ajax-request will simply return this view, filled with the one single record you have updated, like so:
public ActionResult UpdateRecord(Int64 id)
{
var _record = Repository.Get(id);
// logic to hide and update record
return PartialView("TableRow_Partial",_record);
}
For this to work, you have to set InsertionMode
to InsertionMode.ReplaceWith
. All this does is that when you send an ajax request to do something with just one record, you only refresh one item - the one that triggered the ajax call, and then you simply return the new row (One row!) and replace the old one with this new one.
If you really want to stick to sending the whole collection of items back to the client every time you do something with the records, just send the entire rendered table to the client and replace the old one with the new one in a similar manner.