4

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?

TimHorton
  • 865
  • 3
  • 13
  • 33
  • 1
    You cannot use `EditorFor()` - and your generating identical `name` attributes for your 'new' `TargetVm` (the `DefaultModelBinder` only reads the first value and ignores the rest). Refer [this answer](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) for some options –  Mar 09 '17 at 09:22
  • Hi Stephen, Thanks for your answer! It gave me a good idea on how to solve this issue. As I'm limited on three "Targets" I just create them in the constructor, and hide two of them with javascript. Have a nice day! – TimHorton Mar 09 '17 at 09:55
  • A terrible way to handle it –  Mar 09 '17 at 09:58
  • 1
    And your not understanding the `EditorFor()` method anyway - its just `@Html.EditorFor(m => m.Targets)` - no loop - the method generates the correct html for each item in the collection. –  Mar 09 '17 at 10:08
  • Actually you are right, it doesn't really work with EditorFor(). I just tried Html.Partial(), but then I have an empty collection anyways, because the fields are not binded properly. I guess I will try option 2 of your posting... thanks sir! – TimHorton Mar 09 '17 at 10:31
  • I would suggest based on your current knowledge, that you start with Option 1. And refer [this answer](http://stackoverflow.com/questions/40539321/a-partial-view-passing-a-collection-using-the-html-begincollectionitem-helper/40541892#40541892) for a more complete example –  Mar 09 '17 at 10:34
  • Thanks Stephen, it seems to work! only one problem left, the field validation doesn't work anymore. I used the [required] attribute for 2 properties on my TargetVm. Do you have a clue? – TimHorton Mar 09 '17 at 11:01
  • 1
    You obviously did did not read either of my answers I linked to :) - you need to re-parse the validator –  Mar 09 '17 at 11:03
  • haha indeed, unfortunately I missed on that detail. Thanks for your help Stephen! – TimHorton Mar 09 '17 at 12:58

1 Answers1

1

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.

Community
  • 1
  • 1