0

Can you please help out?

Controller "Create" POST method arguments is unable to capture the dynamically generated hidden fields.

After I click submit, the post method (if checked on the browser Network tab / developer tools) shows that the hidden fields are captured (see below image) but when I set a breakpoint on the Create POST method in the controller, the field are sometimes captured less than expected, sometimes not even captured.

The issue comes from when you click on "Remove" button, to remove the hidden field, then the problem start.

If you display the form and just fill that in then submit, everything is fine, all the hidden fields are captured and sent to the model after POST; only when you start deleting one or more hidden fields (from remove button), then it start behaving sometimes it captures the remain field, sometimes it shows 0... even though in the network tab it shows that it posted the correct number of hidden field.

PS : I just tried to delete the hidden input field manually, under Elements tab, dev tools, it has the same behavior as what I said above, I mean the network tab POST shows the correct number being posted, but the controller model capture incorrect number, sometimes nothing being captured.

Classes

public class Product
    {
        [Key]
        public int ProductId { get; set; }

        public int ProductsItemInStock { get; set; }

        public decimal CostPerItem { get; set; }

        public string ItemName { get; set; }

        public IList<ProductInvoice> ProductInvoices { get; set; }
    }


public class Invoice
    {
        [Key]
        public int InvoiceId { get; set; }

        public DateTime CreatedDate { get; set; }

        public Guid CreateByUser { get; set; }

        public int TotalInvoice { get; set; }

        public IList<ProductInvoice> ProductInvoices { get; set; }
    }
    
        public class ProductInvoice
    {
        [Key]
        public int ProductId { get; set; }
        public Product Product { get; set; }

        [Key]
        public int InvoiceId { get; set; }
        public Invoice Invoice { get; set; }
    }
    

Controller

        // GET: Invoices/Create
        public IActionResult Create()
        {
            PopulateProductsDropDownList();
            return View();
        }
    
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("InvoiceId,CreatedDate,CreateByUser,TotalInvoice,ProductInvoices")] Invoice invoice)
        {
            if (ModelState.IsValid)
            {
                _context.Add(invoice);
                await _context.SaveChangesAsync();
                return RedirectToAction(nameof(Index));
            }
            return View(invoice);
        }
        
        private void PopulateProductsDropDownList()
        {
            IEnumerable<SelectListItem> items = _context.Products.Select(c => new SelectListItem
            {
                Value = c.ProductId.ToString() + "-" + c.CostPerItem,
                Text = c.ItemName
            });

            ViewData["Products"] = items;
        }

View

@model GeniiApp.Models.Invoice
@using System.Collections.Generic;

@{
    ViewData["Title"] = "Create";
}

<head>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <link href="~/lib/bootstrap/dist/css/bootstrap.css" rel="stylesheet" />
</head>

<h1>Create</h1>

<h4>Invoice</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Create">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="CreatedDate" class="control-label"></label>
                <input asp-for="CreatedDate" class="form-control" />
                <span asp-validation-for="CreatedDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="CreateByUser" class="control-label"></label>
                <input asp-for="CreateByUser" class="form-control" />
                <span asp-validation-for="CreateByUser" class="text-danger"></span>
            </div>

            <div class="form-group">
                <label class="control-label">Add items to invoice</label>

                <div class="form-select form-select-lg mb-3">

                    @Html.DropDownList("SelectedProducts",
                        new SelectList((System.Collections.IEnumerable)ViewData["Products"], "Value", "Text", new { @class = "form-control dropdown-list" }))

                    <span class="span-add-item">
                        <button type="button" class="btn btn-info float-right" id="btn-add-item">Add</button>
                    </span>
                </div>

            </div>

            <div class="form-group card-frame">

                <div class="card" style="">
                    <div class="card-body">
                        <h5 class="card-title">Items added to invoice</h5>
                        <hr />
                        <span id="main-body-card"></span>
                    </div>
                </div>

            </div>

            <div class="form-group frame-total-invoice">
                <label asp-for="TotalInvoice" class="control-label"></label>
                <input id="total-invoice-input" asp-for="TotalInvoice" class="form-control" readonly />
                <span asp-validation-for="TotalInvoice" class="text-danger"></span>
            </div>

            <div class="form-group">
                <input type="submit" value="Save" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-action="Index">Back to List</a>
</div>


@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.min.js"></script>

    <script>
        $(document).ready(function () {

            var totalInvoice = 0;
            var i = 0;
            var p = 0;

            $("#btn-add-item").click(function () {

                var selectedItem = $('#SelectedProducts :selected').text();
                var getCostPerItem = $('#SelectedProducts :selected').val();
                var productId;
                var itemToRemove;

                productId = getCostPerItem.split("-")[0];
                getCostPerItem = getCostPerItem.split("-")[1];

                getCostPerItem = getCostPerItem.split(".")[0];
                totalInvoice = parseInt(totalInvoice) + parseInt(getCostPerItem);
                itemToRemove = "ProductId_" + productId;
                itemToRemoveClass = "ProductId_" + productId + "_" + p + "_Class";

                $('<div id="created_div" class="' + itemToRemoveClass + '">' + '<div>' + '<b>+</b> ' + selectedItem + ' :  R ' + ' ' + getCostPerItem + '<button type="button"  id="' + productId + '_' + p + '" class="btn btn-danger btn-sm btn-remove-item float-right">Remove</button></div><br></div>').insertAfter('#main-body-card');

                p++;

                $("<input class='" + productId + "' value='" + productId + "' name='ProductInvoices[" + i + "].ProductId' type='hidden' />").insertAfter('#created_div');

                i++;
                
                
                $("#total-invoice-input").val(totalInvoice);
            });


            // Moved inside to test the behaviour
            $(document).on("click", "button", function (event) {

                //console.log(event.target.id);
                var el = event.target.id;

                //alert(el);

                var start = "ProductId_";
                var end = "_Class";
                var entire = start + el + end;

                var id = el.split("_")[0];

                //alert(id)


                $("div." + entire).remove();    // delete the div 
                
                
                // Here is the problem !!!
                // this line deletes the hidden field as well
                // if commented everything works fine, I mean it captures the correct number of hidden field and pass it to the model/controller method
                // if uncommented then the issue starts
                $("input[type='hidden']").remove("." + id);

            });


        });




        // Moved outside
        //$(document).on("click", "a", function (event) {

        //    //console.log(event.target.id);
        //    var el = event.target.id;

        //    alert(el);

        //    var start = "ProductId_";
        //    var end = "_Class";
        //    var entire = start + el + end;

        //    var id = el.split("_")[0];

        //    alert(id)

        //    $("div." + entire).remove();
        //    $("input[type='hidden']").remove("." + id);

        //});

        
    </script>

}

enter image description here

enter image description here

Sophia
  • 37
  • 7
  • Please have a read of [mcve]. Try to limit your code to just the issue; possibly with a derived example (eg a single class with two fields, one gets removed) as long as it demonstrates the issue and doesn't require someone to read 250 lines of code. – freedomn-m Jul 21 '21 at 11:10
  • One possible issue is that you do `$('
    – freedomn-m Jul 21 '21 at 11:16
  • @freedomn-m Thank you freedom for assisting me. I'll definitely follow the minimal example next time. – Sophia Jul 21 '21 at 11:48
  • @freedomn-m I created 5 hidden fields just now, when I click **Add** button, I deleted 3 manually under *Elements* tab (dev tools), when i submit the form, the network tab show 2 hidden fields being Posted (correct number), but the controller model captures 0 hidden field. – Sophia Jul 21 '21 at 11:54
  • So the issue comes when you try to delete the hidden fields. Is it valid/legal in term of programming to delete them in runtime? cause I read a post saying that the browser will detect that as a fraud for security reason. Not sure if it's true. – Sophia Jul 21 '21 at 11:59
  • Ah - your image makes it clear. You haven't just deleted "an input", you've deleted an *indexed* input. In order for the default MVC binder to handle arrays, they have to start at 0 and be contiguous (no gaps). So you *have* to have `ProductInvoices[0].ProductId` and `ProductInvoices[1].ProductId` in order to be able to have `ProductInvoices[2].ProductId` (and `[3]`). – freedomn-m Jul 21 '21 at 12:37
  • This [link-only answer](https://stackoverflow.com/questions/2080355/asp-net-mvc-bind-array-in-model) provides a link to [Model Binding to A List](http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/) and here's a similar page [Wire format for model binding to arrays](https://www.hanselman.com/blog/aspnet-wire-format-for-model-binding-to-arrays-lists-collections-dictionaries) the relevant part is: **The index must be zero-based and unbroken.** – freedomn-m Jul 21 '21 at 12:41
  • One solution is to add an `IsActive` (or similar) flag to each class (or viewmodel equivalent) and a hidden field `name="ProductInvoices[0].IsActive"` which you set to false (or use "Deleted" and set to true, which is potentially easier) - ie don't delete the inputs, but post that that index is not to be used. – freedomn-m Jul 21 '21 at 12:42

2 Answers2

0

This has happened to me multiple times, and the solution is quite simple.

If you are dynamically adding fields to a form make sure the index always starts from 0

In the example above you have ProductInvoices[3].ProductId and ProductInvoices[2].ProductId, to receive data correctly after submitting the form you will need to pass them as:

ProductInvoices[0].ProductId = 3333

ProductInvoices[1].ProductId = 111

When you remove an item from the list you will need to loop through all of them and update the name attribute of each item to reset the index.

vander
  • 138
  • 6
0

Thanks everyone for your help.

I came up with my own solution and solved it.

I asked myself a question : Why would I create a hidden field every time the user Add a new Item from the dropdown list, and later remove it. I just ran away from the tricky part of rebuilding/resetting the index as suggested by user vander

My Solution :

I moved the code that dynamically creates hidden field into submit button, and handled from there. I mean when the user click submit button, I looped thru btn-remove-item to collect their Ids and add it on the line that generates the hidden fields.

In my solution, I only create the hidden fields one time, when the user is about to submit the form, there I'm really sure that the user will not delete any hidden fields as he already clicked "Submit" button.

With this solution, when I Post data, I'm totally sure that the index is zero-based and unbroken.

$("#btn-submit").click(function () {

                var ina = 0;

                $(".btn-remove-item").each(function () {

                    var itemId = this.id;

                    var getProductId = itemId.split("_")[0];

                    $("<input class='" + getProductId + "' value='" + getProductId + "' name='ProductInvoices[" + ina + "].ProductId' type='hidden' />").insertAfter('#created_div');

                    ina++;

                });

            });
Sophia
  • 37
  • 7