So I've written some code to allow adding and removing elements from a collection dynamically in ASP.NET MVC using AJAX. Adding new items to the collection works as expected, but removing does not. The model collection is updated as expected (the appropriate item is removed by index), but the rendered HTML consistently shows that the last item has been removed (rather than the one at the specified index).
For example, let's say I have the following items:
- Foo
- Bar
- Baz
When I click "remove" next to the item named "Foo", I'd expect the resulting rendered HTML to look as follows:
- Bar
- Baz
When I debug through the controller action, this seems to be the case, as the Names collection on the model only contains those items. However, the rendered HTML that is returned to my AJAX handler is:
- Foo
- Bar
I thought the issue might have to do with caching, but nothing I've tried (OutputCache directive, setting cache:false in $.ajax, etc) is working.
Here is the code:
DemoViewModel.cs
namespace MvcPlayground.Models
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public class DemoViewModel
{
public List<string> Names { get; set; }
public DemoViewModel()
{
Names = new List<string>();
}
}
}
DemoController.cs
The apparent issue here is in the RemoveName method. I can verify that the Model property of the PartialViewResult reflects the collection state as I expect it, but once rendered to the client the HTML is NOT as I expect it.
namespace MvcPlayground.Controllers
{
using MvcPlayground.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
public class DemoController : Controller
{
// GET: Demo
public ActionResult Index()
{
var model = new DemoViewModel();
return View(model);
}
[HttpPost]
public ActionResult AddName(DemoViewModel model)
{
model.Names.Add(string.Empty);
ViewData.TemplateInfo.HtmlFieldPrefix = "Names";
return PartialView("EditorTemplates/Names", model.Names);
}
[HttpPost]
public ActionResult RemoveName(DemoViewModel model, int index)
{
model.Names.RemoveAt(index);
ViewData.TemplateInfo.HtmlFieldPrefix = "Names";
var result = PartialView("EditorTemplates/Names", model.Names);
return result;
}
}
}
Names.cshtml
This is the editor template that I am using to render out the list of Names. Works as expected when adding a new item to the collection.
@model List<string>
@for (int i = 0; i < Model.Count; i++)
{
<p>
@Html.EditorFor(m => m[i]) @Html.ActionLink("remove", "RemoveName", null, new { data_target = "names", data_index = i, @class = "link link-item-remove" })
</p>
}
Index.cshtml
This is the initial page that is loaded, nothing too complicated here.
@model MvcPlayground.Models.DemoViewModel
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Demo</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="container-collection" id="names">
@Html.EditorFor(m => m.Names, "Names")
</div>
@Html.ActionLink("Add New", "AddName", "Demo", null, new { data_target = "names", @class = "btn btn-addnew" })
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
Index.js
This script handles the calls to AddName and RemoveName. Everything here works as I'd expect.
$('form').on('click', '.btn-addnew', function (e) {
e.preventDefault();
var form = $(this).closest('form');
var targetId = $(this).data('target');
var target = form.find('#' + targetId);
var href = $(this).attr('href');
$.ajax({
url: href,
cache: false,
type: 'POST',
data: form.serialize()
}).done(function (html) {
target.html(html);
});
});
$('form').on('click', '.link-item-remove', function (e) {
e.preventDefault();
var form = $(this).closest('form');
var targetId = $(this).data('target');
var target = form.find('#' + targetId);
var href = $(this).attr('href');
var formData = form.serialize() + '&index=' + $(this).data('index');
$.ajax({
url: href,
cache: false,
type: 'POST',
data: formData
}).done(function (html) {
target.html(html);
});
});