.NET 5中Bootstrap表格动态行下拉框批量变更问题排查与解决
问题描述
开发发票页面时,通过下拉框填充Bootstrap表格行数据,已实现行插入功能,但存在异常:选择任意行的下拉框选项时,所有行的对应qty和price值都会被修改,而非仅修改当前选中的行。
前端代码(原代码)
<table id="table1" border="0"> <tr style="font-size:80%;"> <th>#</th> <th style="width:50%;">Product</th> <th style="width:5%;">qty</th> <th style="width:5%;">price</th> <th class="d-print-none">X</th> </tr> @{ int i = 0; foreach (var item in Model.PL) { <tr class="border-bottom"> <td class="tbl_id">1</td> <td> <select id="Products" asp-for="@Model.PL[@i].ProdId" class="form-control" type="text" name="data[@i][ProdId]" onchange="copyProd(this)" style="width:100%"> @foreach (var item in Model.PL) { <option value="@item.ProdId" qty="@qty" price="@Convert.ToInt32(item.price)"> @item.name, @Convert.ToInt32(item.price) </option> } </select> <script> function copyProd(el) { let op = el.options[el.selectedIndex]; let myvalue = el.value; let qty = op.getAttribute('qty'); let price = op.getAttribute('price'); $("input[name='qty[@i]']").val(qty); $("input[name='price[@i]']").val(price); } </script> </td> </tr> } i++; } </table> <script> //JS class to add/delete rows in html table function adRowsTable(id) { var table = document.getElementById(id); var me = this; if (document.getElementById(id)) { var row1 = table.rows[1].outerHTML; // set index-id in cols with class .tbl_id function setIds() { var tbl_id = document.querySelectorAll('#' + id + ' .tbl_id'); for (var i = 0; i < tbl_id.length; i++) tbl_id[i].innerHTML = i + 1; } // add row after clicked row; receives clicked button in row me.addRow = function (btn) { btn ? btn.parentNode.parentNode.insertAdjacentHTML('afterend', row1) : table.insertAdjacentHTML('beforeend', row1); setIds(); } // delete clicked row; receives clicked button in row me.delRow = function (btn) { btn.parentNode.parentNode.outerHTML = ''; setIds(); } } } //create object of adRowsTable(), pass the table id var ob_adRows = new adRowsTable('table1'); </script>
后端代码(原代码)
public async Task<IActionResult> OnGetAsync() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); try { PL = await _context.Products.ToListAsync(); var addrows = new List<InvoiceRows>() { new Products() { ProdId = 1, qty = 1, price = 0 }, new Products() { ProdId = 1, qty = 1, price = 0 }, new Products() { ProdId = 1, qty = 1, price = 0 }, }; PL = addrows; ViewData["ProdId"] = new SelectList(_context.Products, "ProdId", "Name"); } catch (Exception e) { _ = e.ToString(); } return Page(); } [BindProperty] public InvoiceRows InvoiceRow { get; set; } = default!; public IList<Products> PL { get; set; } = default!; public async Task<IActionResult> OnPostAsync(List<InvoiceRows> addRow) { _context.InvoiceRows.AddRange(addRow); await _context.SaveChangesAsync(); return RedirectToPage("./Index"); }
问题分析与解决方案
1. 核心问题:循环变量作用域与重复函数定义
- 问题:在Razor的
foreach循环中,每次都会生成一个copyProd函数,且函数内引用的循环变量i会被最后一次循环的值覆盖,导致所有下拉框触发时都会修改同一组输入框。 - 解决:
- 将
copyProd函数移到循环外部,只定义一次。 - 给每个下拉框添加自定义属性
data-row-index="@i",绑定当前行的索引,在函数中通过该属性获取目标行。
- 将
2. 动态行的事件与属性更新
- 问题:动态插入的行,下拉框的
onchange事件无法生效,且输入框的name属性索引固定,无法匹配当前行数。 - 解决:
- 使用事件委托监听下拉框变化,确保动态添加的行也能触发事件。
- 创建行模板,插入时动态替换索引值,并在
setIds函数中更新所有行的索引属性与name值。
3. 其他细节问题
- 下拉框ID重复:将ID改为
Products_@i保证唯一性,或直接移除ID(无需通过ID选择时)。 - 后端类型不匹配:原代码中
PL是IList<Products>,却被赋值为List<InvoiceRows>,需修正类型对应关系。
修改后的代码示例
前端Razor视图
<table id="table1" border="0"> <tr style="font-size:80%;"> <th>#</th> <th style="width:50%;">Product</th> <th style="width:5%;">qty</th> <th style="width:5%;">price</th> <th class="d-print-none">操作</th> </tr> @{ int i = 0; foreach (var item in Model.PL) { <tr class="border-bottom"> <td class="tbl_id">@(i+1)</td> <td> <select id="Products_@i" asp-for="@Model.PL[i].ProdId" class="form-control product-select" name="data[@i][ProdId]" data-row-index="@i" style="width:100%"> @foreach (var prod in Model.ProductsList) { <option value="@prod.ProdId" qty="@prod.qty" price="@Convert.ToInt32(prod.price)"> @prod.name, @Convert.ToInt32(prod.price) </option> } </select> </td> <td> <input type="number" name="qty[@i]" class="form-control qty-input" value="@item.qty" /> </td> <td> <input type="number" name="price[@i]" class="form-control price-input" value="@item.price" /> </td> <td class="d-print-none"> <button onclick="ob_adRows.delRow(this)">删除</button> </td> </tr> i++; } } </table> <button onclick="ob_adRows.addRow()">添加行</button> <script> // 全局定义一次函数 function copyProd(el) { let op = el.options[el.selectedIndex]; let rowIndex = el.getAttribute('data-row-index'); let qty = op.getAttribute('qty'); let price = op.getAttribute('price'); $(`input[name='qty[${rowIndex}]']`).val(qty); $(`input[name='price[${rowIndex}]']`).val(price); } // 事件委托处理动态行的下拉框变化 $(document).on('change', '.product-select', function() { copyProd(this); }); // 行操作类 function adRowsTable(id) { var table = document.getElementById(id); var me = this; if (document.getElementById(id)) { // 行模板,用{index}作为占位符 var templateRow = ` <tr class="border-bottom"> <td class="tbl_id"></td> <td> <select class="form-control product-select" name="data[{index}][ProdId]" data-row-index="{index}" style="width:100%"> ${document.querySelector('.product-select').innerHTML} </select> </td> <td> <input type="number" name="qty[{index}]" class="form-control qty-input" value="1" /> </td> <td> <input type="number" name="price[{index}]" class="form-control price-input" value="0" /> </td> <td class="d-print-none"> <button onclick="ob_adRows.delRow(this)">删除</button> </td> </tr> `; function setIds() { var rows = document.querySelectorAll('#' + id + ' tr.border-bottom'); for (var i = 0; i < rows.length; i++) { // 更新行号 rows[i].querySelector('.tbl_id').innerHTML = i + 1; // 更新索引属性与name值 var selectEl = rows[i].querySelector('.product-select'); selectEl.setAttribute('data-row-index', i); selectEl.setAttribute('name', `data[${i}][ProdId]`); rows[i].querySelector('.qty-input').setAttribute('name', `qty[${i}]`); rows[i].querySelector('.price-input').setAttribute('name', `price[${i}]`); } } me.addRow = function (btn) { var rowCount = document.querySelectorAll('#' + id + ' tr.border-bottom').length; var newRow = templateRow.replace(/\{index\}/g, rowCount); btn ? btn.parentNode.parentNode.insertAdjacentHTML('afterend', newRow) : table.insertAdjacentHTML('beforeend', newRow); setIds(); } me.delRow = function (btn) { btn.parentNode.parentNode.outerHTML = ''; setIds(); } } } var ob_adRows = new adRowsTable('table1'); </script>
后端代码
public async Task<IActionResult> OnGetAsync() { var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); try { // 获取产品列表用于下拉框选项 ProductsList = await _context.Products.ToListAsync(); // 初始化发票行数据 PL = new List<InvoiceRows>() { new InvoiceRows() { ProdId = 1, qty = 1, price = 0 }, new InvoiceRows() { ProdId = 1, qty = 1, price = 0 }, new InvoiceRows() { ProdId = 1, qty = 1, price = 0 }, }; ViewData["ProdId"] = new SelectList(_context.Products, "ProdId", "Name"); } catch (Exception e) { _ = e.ToString(); } return Page(); } [BindProperty] public InvoiceRows InvoiceRow { get; set; } = default!; // 发票行列表 public IList<InvoiceRows> PL { get; set; } = default!; // 产品列表(用于下拉框) public IList<Products> ProductsList { get; set; } = default!; public async Task<IActionResult> OnPostAsync(List<InvoiceRows> addRow) { if (addRow != null && addRow.Any()) { _context.InvoiceRows.AddRange(addRow); await _context.SaveChangesAsync(); } return RedirectToPage("./Index"); }
内容的提问来源于stack exchange,提问作者Fillo
相关产品推荐
相关产品推荐

