I'm currently working on an ASP.NET MVC 4.5 application. I want to add objects to a List<T> dynamically on a button click, using a @Html.EditorFor and send the data to the Controller with a Form submit.
However, my problem, only the 1st element of the populated list gets sent to the controller when I submit my Form.
The area in my view where I add Elements looks like this:
<div id="addTarget" data-target="@Url.Action("AddTarget", "Offer")">
@for(var i = 0; i < Model.Targets.Count(); i++)
{
@Html.EditorFor(m => m.Targets[i])
}
</div>
My Controller looks like that:
...
public OfferVm NewOffer { get; set; } = new OfferVm();
[HttpGet]
public ActionResult Create()
{
// ...here I add my first initial target to OfferVm
NewOffer.Targets.Add(new TargetVm());
return View(NewOffer);
}
public ActionResult AddTarget()
{
// ...here I add a target to my NewOffer
NewOffer.Targets.Add(new TargetVm());
return PartialView("~/Views/Shared/EditorTemplates/TargetVm.cshtml");
}
[HttpPost]
public ActionResult Create(OfferVm offerVM)
{
//... here only 1 target is in my offerVM
}
My OfferVm class looks like this:
public class OfferVm
{
public OfferVm()
{
this.Targets = new List<TargetVm>();
}
public List<TargetVm> Targets { get; set; }
}
Do you have an idea how I can add a new target to my NewOffer.Targets List, and get the data in my controller, when I post the form?
The reason only the first TargetVm in your collection is being bound in the POST method, is that your AddTarget() method your calling to add a new item is generating form controls with name that are identical to those your have previously generated. Assuming TargetVm has a property named ID, then your have multiple elements with
<input ... name="Targets[0].ID" ... />
and the DefaultModelBinder only binds the first name/value pair from the request, and ignores any subsequent duplicate names. In order to bind to a collection, your name attributes need to have indexers that start at zero and be consecutive.
<input ... name="Targets[0].ID" ... /> // first TargetVM
<input ... name="Targets[1].ID" ... /> // second TargetVM
<input ... name="Targets[2].ID" ... /> // third TargetVM
Note you can also include an extra input for each item in the collection - <input type="hidden" name="Targets.Index value="xx" /> (where xx is the value of the indexer) to bind non-consecutive indexers as discussed in Model Binding To A List.
Using the EditorFor() method will not work in the case where your dynamically adding new collection items, unless you were to use javascript to update the value of the indexer in the name attribute when its added.
Solutions include using a javascript MVVM framework such as knockout, using the BeginCollectionItem() helper, or using a client side template and javascript to dynamically add new form controls. The latter two options are discussed in this answer, and a more complete example using BeginCollectionItem() is shown in this answer.