3

Background

I am building a fairly simple Inventory and Order Management System. In all such systems, you eventually get to the bit where an Order (PurchaseOrder in my case) has a list of Order Line Items (called LineItem in my application).

Backend Code

For now, my ViewModels map directly to my Entities, and so my PurchaseOrderVm looks like this:

public class PurchaseOrderVm
{
  public PurchaseOrderVm()
  {
    LineItems = new List<LineItemVm>();
  }

  public int Id { get; set; }
  public DateTime Date { get; set; }
  public PurchaseOrderStatus Status { get; set; }
  [Display(Name = "Shipping Cost")]
  public decimal ShippingCost { get; set; }
  [Display(Name = "Import VAT")]
  public decimal Vat { get; set; }
  [Display(Name = "Import Duty")]
  public decimal ImportDuty { get; set; }
  [Display(Name = "Supplier Id")]
  public string SupplierId { get; set; }
  [Display(Name = "Supplier")]
  public string SupplierName { get; set; }
  public IEnumerable<LineItemVm> LineItems { get; set; }

  public List<SelectListItem> Suppliers { get; set; }

  public string ButtonText => Id != 0 ? "Update Purchase Order" : "Add Purchase Order";
}

And my LineItemVm looks like this:

public class LineItemVm
{
  public int Id { get; set; }
  public int Quantity { get; set; }
  public int OrderId { get; set; }
  [Display(Name = "Product")]
  public int ProductId { get; set; }
}

Front-End / UX

I know the type of experience I want to build, but I'm not sure how much things have moved on with this aspect since MVC3. I want to build this (bits highlighted for importance):

order management

Tried

I was able to get this working for a single LineItem only by doing this:

<input class="form-control " id="ProductId" name="LineItems[0].ProductId" type="number" value="">

But obviously this is the opposite of dynamic. What is the cleanest way of allowing users to add and remove items as they wish and have the form and model binder still work?

halfer
  • 19,824
  • 17
  • 99
  • 186
J86
  • 14,345
  • 47
  • 130
  • 228
  • You can check this link about collections. http://www.c-sharpcorner.com/uploadfile/pmfawas/asp-net-mvc-how-to-post-a-collection/ What you're going to to ` for (int i = 0; i < Model.LineItems.Count; i++) { Html.TextBoxFor(m=>Model.LineItems[i].ProductId) } ` – Miguel Nov 13 '16 at 15:34
  • Which frontend framework are you using?? – Nico Beemster Nov 13 '16 at 16:08
  • None at the moment, I was hoping just vanilla JS? – J86 Nov 13 '16 at 16:39
  • Refer [this answer](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) for some options –  Nov 13 '16 at 20:48
  • 1
    Hi Ciwan. How did you get on with the extensive and helpful answer below? Did it assist you? – halfer Sep 25 '17 at 12:50

1 Answers1

4

You can basically build new line item rows using client side javascript code. You need to make sure that the input element(product and quantity) has the correct name attribute value so that model binding will work.

For model binding to work, your inputs name should be like LineItems[0].ProductId, LineItems[0].Quantity (for row1), LineItems[1].ProductId, LineItems[1].Quantity (for row2) etc.

Since you need a product dropdown for each row, we will keep a select element in the page and whenever we need to add a new line item row, we will clone this element and use that for the row. We will hide this select with css because this is more like a look up data for us to clone as needed.

@model PurchaseOrderVm 
@using (Html.BeginForm("Add","Product"))
{
    @Html.LabelFor(f=>f.SupplierName)
    @Html.TextBoxFor(s=>s.SupplierName)

    <label>Order Products</label>
    <table id="items">
        <thead>
            <tr>
                <th>Product</th><th>Quantity</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
    <button id="addProduct">Add Product</button>
    <input type="submit"/>
}
<SELECT Id="Products">
    <option value="1">Product 1</option>
    <option value="2">Product 2</option>
    <option value="3">Product 3</option>
</SELECT>

I hard coded the contents of products select element. you can replace it with data coming from your server using the Html.DropDownListFor/Html.DropDownList helper method(s)

Now, listen to the click event on the addProduct button, create a new row, add the select element and quantity input element and append it to the table. When the remove link is clicked, simply remove the closest row and re organize the name values of other inputs we created.

$(function () {          

    $('#addProduct').click(function (e) {
        e.preventDefault();
        var rowIndex = $("#items>tbody>tr").length;
        var newRow = $("<tr/>");
        var p = $("#Products").clone()
              .attr("name", "LineItems[" + rowIndex + "].ProductId")
              .data("n","LineItems[r].ProductId").show();

        $("<td/>").append(p).appendTo(newRow);
        var q = $("<input />")
             .attr("name", "LineItems[" + rowIndex + "].Quantity")
             .data("n","LineItems[r].Quantity");
        var r = $("<a/>").attr("class", "remove").text("Remove");
        $("<td/>").append(q).append(r).appendTo(newRow);

        $("#items>tbody").append(newRow);

    });

    $("#items").on("click",".remove",function(e) {
        e.preventDefault();
        $(this).closest("tr").remove();
        //Re organize the input names
        $("#items>tbody>tr").each(function (rowIndex, row) {
            $(row).find("input").each(function(a, b) {
                var newName = $(b).data("n").replace("r", rowIndex);
                $(b).attr("name", newName);
            });
        });
    });

});

Now when the form is submitted, LineItems property of your view model will have the correct data.

[HttpPost]
public ActionResult Add(PurchaseOrderVm model)
{
  // check model.LineItems
  // to do : Return something
}
Shyju
  • 214,206
  • 104
  • 411
  • 497