39

I'm developing an ASP.NET MVC 5 web with C# and .NET Framework 4.5.1.

I have this form in a cshtml file:

@model MyProduct.Web.API.Models.ConnectBatchProductViewModel

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Create</title>
</head>
<body>
    @if (@Model != null)
    { 
        <h4>Producto: @Model.Product.ProductCode, Cantidad: @Model.ExternalCodesForThisProduct</h4>
        using (Html.BeginForm("Save", "ConnectBatchProduct", FormMethod.Post))
        {
            @Html.HiddenFor(model => model.Product.Id, new { @id = "productId", @Name = "productId" });

            <div>
                <table id ="batchTable" class="order-list">
                    <thead>
                        <tr>
                            <td>Cantidad</td>
                            <td>Lote</td>
                        </tr>
                    </thead>
                    <tbody>
                        <tr>
                            <td>@Html.TextBox("ConnectBatchProductViewModel.BatchProducts[0].Quantity")</td>
                            <td>@Html.TextBox("ConnectBatchProductViewModel.BatchProducts[0].BatchName")</td>
                            <td><a class="deleteRow"></a></td>
                        </tr>
                    </tbody>
                    <tfoot>
                        <tr>
                            <td colspan="5" style="text-align: left;">
                                <input type="button" id="addrow" value="Add Row" />
                            </td>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <p><input type="submit" value="Seleccionar" /></p>
        }
    }
    else
    { 
        <div>Error.</div>
    }
<script src="~/Scripts/jquery-2.1.3.min.js"></script>
<script src="~/js/createBatches.js"></script> <!-- Resource jQuery -->    
</body>
</html>

And this is the action method:

[HttpPost]
public ActionResult Save(FormCollection form)
{
    return null;
}

And the two ViewModel:

public class BatchProductViewModel
{
    public int Quantity { get; set; }
    public string BatchName { get; set; }
}

public class ConnectBatchProductViewModel
{
    public Models.Products Product { get; set; }
    public int ExternalCodesForThisProduct { get; set; }

    public IEnumerable<BatchProductViewModel> BatchProducts { get; set; }
}

But I get this in FormCollection form var: enter image description here

But I want to get an IEnumerable<BatchProductViewModel> model:

public ActionResult Save(int productId, IEnumerable<BatchProductViewModel> model);

If I use the above method signature both parameters are null.

I want an IEnumerable because user is going to add more rows dynamically using jQuery.

This is jQuery script:

jQuery(document).ready(function ($) {
    var counter = 0;

    $("#addrow").on("click", function () {

        counter = $('#batchTable tr').length - 2;

        var newRow = $("<tr>");
        var cols = "";

        var quantity = 'ConnectBatchProductViewModel.BatchProducts[0].Quantity'.replace(/\[.{1}\]/, '[' + counter + ']');
        var batchName = 'ConnectBatchProductViewModel.BatchProducts[0].BatchName'.replace(/\[.{1}\]/, '[' + counter + ']');

        cols += '<td><input type="text" name="' + quantity + '"/></td>';
        cols += '<td><input type="text" name="' + batchName + '"/></td>';

        cols += '<td><input type="button" class="ibtnDel"  value="Delete"></td>';
        newRow.append(cols);

        $("table.order-list").append(newRow);
        counter++;
    });


    $("table.order-list").on("click", ".ibtnDel", function (event) {
        $(this).closest("tr").remove();

        counter -= 1
        $('#addrow').attr('disabled', false).prop('value', "Add Row");
    });
});

Any idea?

I have checked this SO answer, and this article but I don't get my code working.

Community
  • 1
  • 1
VansFannel
  • 41,682
  • 96
  • 329
  • 561
  • Please post the code for `BatchProductViewModel` as well... Also, in your action method, are you sure your intent was to use `BatchProductViewModel` and not `ConnectBatchProductViewModel`? – Ruslan Mar 20 '15 at 07:37
  • @Ruslan Question updated. – VansFannel Mar 20 '15 at 07:38
  • Your model in the view is `ConnectBatchProductViewModel` If you want to generate a view for a collection of `BatchProductViewModel` then you view needs to be `IEnumerable` and the POST method parameter needs to be the same (don't use `FormCollection`) and the controls for `BatchProductViewModel` need to be generated in a `for` loop –  Mar 20 '15 at 07:42
  • And `int productId` will never be bound because the name of your control is `Product.Id` (not productId). If you change the method to `public ActionResult Save(ConnectBatchProductViewModel model)` you will see that it is correctly bound –  Mar 20 '15 at 07:45
  • I have updated my question. I have changed the view model and I getting the same result. I want an `IEnumerable` because I want to add more rows dynamically using jQuery. – VansFannel Mar 20 '15 at 07:46

5 Answers5

31

You need to generate the controls for the collection in a for loop so they are correctly named with indexers (note that property BatchProducts needs to be IList<BatchProductViewModel>

@using (Html.BeginForm("Save", "ConnectBatchProduct", FormMethod.Post))
{
  ....
  <table>
    ....
    @for(int i = 0; i < Model.BatchProducts.Count; i++)
    {
      <tr>
        <td>@Html.TextBoxFor(m => m.BatchProducts[i].Quantity)</td>
        <td>@Html.TextBoxFor(m => m.BatchProducts[i].BatchName)</td>
        <td>
          // add the following to allow for dynamically deleting items in the view
          <input type="hidden" name="BatchProducts.Index" value="@i" />
          <a class="deleteRow"></a>
        </td>
      </tr>
    }
    ....
  </table>
  ....
}

Then the POST method needs to be

public ActionResult Save(ConnectBatchProductViewModel model)
{
  ....
}

Edit

Note: Further to your edit, if you want to dynamically add and remove BatchProductViewModel items in he view, you will need to use the BeginCollectionItem helper or a html template as discussed in this answer

The template to dynamically add new items would be

<div id="NewBatchProduct" style="display:none">
  <tr>
    <td><input type="text" name="BatchProducts[#].Quantity" value /></td>
    <td><input type="text" name="BatchProducts[#].BatchName" value /></td>
    <td>
      <input type="hidden" name="BatchProducts.Index" value ="%"/>
      <a class="deleteRow"></a>
    </td>
  </tr>
</div>

Note the dummy indexers and the non-matching value for the hidden input prevents this template posting back.

Then the script to add a new BatchProducts would be

$("#addrow").click(function() {
  var index = (new Date()).getTime(); // unique indexer
  var clone = $('#NewBatchProduct').clone(); // clone the BatchProducts item
  // Update the index of the clone
  clone.html($(clone).html().replace(/\[#\]/g, '[' + index + ']'));
  clone.html($(clone).html().replace(/"%"/g, '"' + index  + '"'));
  $("table.order-list").append(clone.html());
});
Community
  • 1
  • 1
  • I want an `IEnumerable` because user is going to add more rows dynamically using jQuery. – VansFannel Mar 20 '15 at 07:53
  • 1
    See update to some techniques to do this (and it does not need `IEnumerable` - it can just as easily be `IList`) –  Mar 20 '15 at 07:54
  • Note that your model does not have a property named `ConnectBatchProductViewModel` so `ConnectBatchProductViewModel.BatchProducts[#].Quantity` wont bind to anything. It needs to be `BatchProducts[#].Quantity` in order to bind. –  Mar 20 '15 at 08:04
  • Next, you don't generate a `` control for each item, so as soon as you delete an item, binding will fail. –  Mar 20 '15 at 08:06
  • Without adding `BeginCollectionItem` and using `@Html.TextBox("BatchProducts[0].Quantity")` it works. I get a all data correctly inside the `ConnectBatchProductViewModel model` parameter. But if I have three rows and user deletes second row, I don't get all data in `model` parameter. – VansFannel Mar 20 '15 at 08:22
  • Yes, but it you are also wanting to delete items as indicated in your script, you also need the special input for the index property. By default the `ModelBinder` will only bind collections where the indexer starts at zero and is consecutive - test it by deleting the first item and the collection will be empty (adding the hidden input for `BatchProducts.Index` overrides that). Also do not base you indexer on the count of the items - if you delete one item, then add another you will have duplicates and binding will also fail –  Mar 20 '15 at 08:27
  • Answer updated with suggested code to allow dynamically adding and deleting items –  Mar 20 '15 at 08:46
  • Thanks a lot for your time an effort. You've helped me a lot. – VansFannel Mar 20 '15 at 09:38
  • Finally, after Learning ViewModels I was able to use this. – Aizen Apr 18 '15 at 17:40
0

In your Post Methode you receive "MyProduct.Web.API.Models.ConnectBatchProductViewModel" as Parameter.
Use the existing model for the Post methode.

Why do you want a IEnumerable from your model? there is only one available including the id in the model.

Andre
  • 560
  • 1
  • 8
  • 18
0

you can visit this article for complete source code with a video tutorial.

you have to create an action first, from where we can pass the list of object

[HttpGet]
public ActionResult Index()
{
    List<Contact> model = new List<Contact>();
    using (MyDatabaseEntities dc = new MyDatabaseEntities())
    {
        model = dc.Contacts.ToList();
    }
    return View(model);
}

then we need to create a view for that action

@model List<UpdateMultiRecord.Contact>
@{
    ViewBag.Title = "Update multiple row at once Using MVC 4 and EF ";
}
@using (@Html.BeginForm("Index","Home", FormMethod.Post))
{
    <table>
            <tr>
                <th></th>               
                <th>Contact Person</th>
                <th>Contact No</th>
                <th>Email ID</th>
            </tr>
        @for (int i = 0; i < Model.Count; i++)
        {
            <tr>               
                <td> @Html.HiddenFor(model => model[i].ContactID)</td>
                <td>@Html.EditorFor(model => model[i].ContactPerson)</td>
                <td>@Html.EditorFor(model => model[i].Contactno)</td>
                <td>@Html.EditorFor(model => model[i].EmailID)</td>
            </tr>
        }
    </table>
    <p><input type="submit" value="Save" /></p>
    <p style="color:green; font-size:12px;">
        @ViewBag.Message
    </p>
}
 @section Scripts{
    @Scripts.Render("~/bundles/jqueryval")
 }

and then we have to write code for save the list of object to the database

[HttpPost]
public ActionResult Index(List<Contact> list)
{  
    if (ModelState.IsValid)
    {
        using (MyDatabaseEntities dc = new MyDatabaseEntities())
        {
            foreach (var i in list)
            {
                var c = dc.Contacts.Where(a =>a.ContactID.Equals(i.ContactID)).FirstOrDefault();
                if (c != null)
                {
                    c.ContactPerson = i.ContactPerson;
                    c.Contactno = i.Contactno;
                    c.EmailID = i.EmailID;
                }
            }
            dc.SaveChanges();
        }
        ViewBag.Message = "Successfully Updated.";
        return View(list);
    }
    else
    {
        ViewBag.Message = "Failed ! Please try again.";
        return View(list);
    }
}
Sourav Mondal
  • 415
  • 6
  • 13
0

using(Html.BeginForm())
{
  // code here 

}

While to Post form Data all tags must be included form tag.

0

Following the principle of DRY, you can create one EditorTemplate for that purpose. Steps:

1- In Views > Shared > Create new folder named (EditorTemplates)

2- Create a view inside your newly created EditorTemplates folder , the view's model should be BatchProductViewModel according to the OP example. Place your code inside the Editor view. No loop or index is required.

An EditorTemplate will act similar to a PartialView for every child entity but in a more generic way.

3- In your parent entity's view, call your Editor : @Html.EditorFor(m => m.BatchProducts)

Not only this provides a more organized views, but also let's you re-use the same editor in other views as well.

Mohamed Nagieb
  • 681
  • 8
  • 10