28

I have added a button in my view. When this button is clicked partial view is added. In my form I can add as much partial view as I can. When Submitting this form data I am unable to send all the partial view data to controller. I have made a different model having all the attributes and I have made a list of that model to my main model. Can anyone please give me some trick so that I can send all the partial view content to my controller?

In My View

<div id="CSQGroup">   
</div>
<div>
  <input type="button" value="Add Field" id="addField" onclick="addFieldss()" />
</div>

function addFieldss()
{    
  $.ajax({
    url: '@Url.Content("~/AdminProduct/GetColorSizeQty")',
    type: 'GET',
    success:function(result) {
      var newDiv = $(document.createElement("div")).attr("id", 'CSQ' + myCounter);  
      newDiv.html(result);
      newDiv.appendTo("#CSQGroup");
      myCounter++;
    },
    error: function(result) {
      alert("Failure");
    }
  });
}

In My controller

public ActionResult GetColorSizeQty()
{
  var data = new AdminProductDetailModel();
  data.colorList = commonCore.getallTypeofList("color");
  data.sizeList = commonCore.getallTypeofList("size");
  return PartialView(data);
}

[HttpPost]
public ActionResult AddDetail(AdminProductDetailModel model)
{
  ....
}

In my Partial View

@model IKLE.Model.ProductModel.AdminProductDetailModel
<div class="editor-field">
  @Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
  @Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
  @Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
</div>
<div class="editor-field">
  @Html.LabelFor(model => model.fkConfigChoiceCategoryColorId)
  @Html.DropDownListFor(model => model.fkConfigChoiceCategoryColorId, Model.colorList, "--Select Color--")
  @Html.ValidationMessageFor(model => model.fkConfigChoiceCategoryColorId)
</div>   
<div class="editor-field">
  @Html.LabelFor(model => model.productTotalQuantity)
  @Html.TextBoxFor(model => model.productTotalQuantity)
  @Html.ValidationMessageFor(model => model.productTotalQuantity)
</div>
Brajesh
  • 441
  • 1
  • 5
  • 14
  • Please review the code and if anything more is required to see please let me know – Brajesh Jan 19 '15 at 07:35
  • What is the POST method for the form? You mentioned a collection, but the partial view your generating wont post back to a collection (the name attributes do not contain indexers for binding to a collection) –  Jan 19 '15 at 07:39
  • using (Html.BeginForm("AddDetail", "AdminProduct", FormMethod.Post, new { @enctype = "multipart/form-data" })) { .. form data } do u mean this?? – Brajesh Jan 19 '15 at 07:47
  • No I mean what is the signature of the `AddDetail` method –  Jan 19 '15 at 07:49
  • Ya exactly my partial view must post collection but it dont. Can u give me any trick so that my partial view posts collection – Brajesh Jan 19 '15 at 07:49
  • Signature is public ActionResult AddDetail(AdminProductDetailModel model) { //code block } – Brajesh Jan 19 '15 at 07:50
  • Start by looking at [this answer](http://stackoverflow.com/questions/24026374/adding-another-pet-to-a-model-form/24027152#24027152) –  Jan 19 '15 at 08:03
  • Thank You Stephen. I will check it and let you know if any other problem – Brajesh Jan 19 '15 at 08:14
  • There is a bit more to it than just what's in that answer including re-parsing the validator so you get client side validation for dynamically added content, so I'll post a more detailed answer later –  Jan 19 '15 at 08:19

1 Answers1

55

Your problem is that the partial renders html based on a single AdminProductDetailModel object, yet you are trying to post back a collection. When you dynamically add a new object you continue to add duplicate controls that look like <input name="productTotalQuantity" ..> (this is also creating invalid html because of the duplicate id attributes) where as they need to be <input name="[0].productTotalQuantity" ..>, <input name="[1].productTotalQuantity" ..> etc. in order to bind to a collection on post back.

The DefaultModelBinder required that the indexer for collection items start at zero and be consecutive, or that the form values include a Index=someValue where the indexer is someValue (for example <input name="[ABC].productTotalQuantity" ..><input name="Index" value="ABC">. This is explained in detail in Phil Haack's article Model Binding To A List. Using the Index approach is generally better because it also allows you to delete items from the list (otherwise it would be necessary to rename all existing controls so the indexer is consecutive).

Two possible approaches to your issue.

Option 1

Use the BeginItemCollection helper for your partial view. This helper will render a hidden input for the Index value based on a GUID. You need this in both the partial view and the loop where you render existing items. Your partial would look something like

@model IKLE.Model.ProductModel.AdminProductDetailModel
@using(Html.BeginCollectionItem()) 
{
  <div class="editor-field">
    @Html.LabelFor(model => model.fkConfigChoiceCategorySizeId)
    @Html.DropDownListFor(model => model.fkConfigChoiceCategorySizeId, Model.sizeList, "--Select Size--")
    @Html.ValidationMessageFor(model => model.fkConfigChoiceCategorySizeId)
  </div>
  ....
}

Option 2

Manually create the html elements representing a new object with a 'fake' indexer, place them in a hidden container, then in the Add button event, clone the html, update the indexers and Index value and append the cloned elements to the DOM. To make sure the html is correct, create one default object in a for loop and inspect the html it generates. An example of this approach is shown in this answer

<div id="newItem" style="display:none">

  <div class="editor-field">
    <label for="_#__productTotalQuantity">Quantity</label>
    <input type="text" id="_#__productTotalQuantity" name="[#].productTotalQuantity" value />
    ....
  </div>
  // more properties of your model
</div>

Note the use of a 'fake' indexer to prevent this one being bound on post back ('#' and '%' wont match up so they are ignored by the DefaultModelBinder)

$('#addField').click(function() {
  var index = (new Date()).getTime(); 
  var clone = $('#NewItem').clone();
  // Update the indexer and Index value of the clone
  clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
  clone.html($(clone).html().replace(/"%"/g, '"' + index  + '"'));
  $('#yourContainer').append(clone.html());
}

The advantage of option 1 is that you are strongly typing the view to your model, but it means making a call to the server each time you add a new item. The advantage of option 2 is that its all done client side, but if you make any changes to you model (e.g. add a validation attribute to a property) then you also need to manually update the html, making maintenance a bit harder.

Finally, if you are using client side validation (jquery-validate-unobtrusive.js), then you need re-parse the validator each time you add new elements to the DOM as explained in this answer.

$('form').data('validator', null);
$.validator.unobtrusive.parse($('form'));

And of course you need to change you POST method to accept a collection

[HttpPost]
public ActionResult AddDetail(IEnumerable<AdminProductDetailModel> model)
{
  ....
}
Community
  • 1
  • 1
  • Thank You for help but I already solved this problem via the technique you have mentioned. – Brajesh Jan 22 '15 at 07:06
  • ah thanks. I haven't really come to the point where I need this but this will certainly come handy when I need it. ;) – Ian Aug 19 '16 at 10:50
  • Anyone else coming here I strongly recommend the BeginItemCollection helper. It feels like cheating. – Preston Sep 09 '16 at 17:27
  • As a note, there's also a [BeginCollectionItemCore](https://github.com/saad749/BeginCollectionItemCore) package for ASP.NET Core. Works for me just fine. – erikbozic Jan 18 '17 at 10:05
  • 1
    I'm not sure if this is a change in `BeginCollectionItem` since this was originally posted, but it's not mentioned here so I'll bring it up: `BeginCollectionItem` asks for a string parameter named `collectionName`. This has to match the name of the collection you are trying to link to. For example, my `ParentViewModel` has a list named `ChildViewModels`, so in my Child Partial View I will have `@using(Html.BeginCollectionItem("ChildViewModels")`. – Ryan Taite Dec 11 '17 at 19:18
  • You just saved my Day :) your option 1 was the best for my case. – Helbo Jun 21 '18 at 12:08