基于ASP.NET Core开发可新增行项目的订单类编辑页面遇阻
Hey Scott, I've built exactly this kind of order form multiple times in ASP.NET Core—let's break this down into manageable parts so you can get it working smoothly.
Step 1: Define Your Models
First, we need models to represent the order header, line items, and a view model to tie them together for the form:
// OrderHeader.cs public class OrderHeader { public string CustomerName { get; set; } public string CustomerEmail { get; set; } public DateTime OrderDate { get; set; } = DateTime.Now; } // OrderLineItem.cs public class OrderLineItem { public int ProductId { get; set; } public int Quantity { get; set; } = 1; } // OrderViewModel.cs (used for form binding) public class OrderViewModel { public OrderHeader Header { get; set; } = new OrderHeader(); public List<OrderLineItem> LineItems { get; set; } = new List<OrderLineItem>(); }
Step 2: Set Up the Controller
Create a controller with actions to display the form and handle submission. We'll also pass product data for the dropdown:
public class OrderController : Controller { // Mock product data—replace with your actual database/service call private readonly List<Product> _products = new List<Product> { new Product { Id = 1, Name = "Laptop" }, new Product { Id = 2, Name = "Wireless Mouse" }, new Product { Id = 3, Name = "Mechanical Keyboard" } }; public IActionResult Create() { var viewModel = new OrderViewModel(); // Add one initial line item to the form viewModel.LineItems.Add(new OrderLineItem()); // Pass products to view for the dropdown ViewBag.Products = new SelectList(_products, "Id", "Name"); return View(viewModel); } [HttpPost] public IActionResult Create(OrderViewModel viewModel) { if (ModelState.IsValid) { // Process your order here—save to database, trigger workflows, etc. // Example: _orderRepository.SaveOrder(viewModel); return RedirectToAction("OrderSuccess"); } // If validation fails, repopulate products and return the form ViewBag.Products = new SelectList(_products, "Id", "Name"); return View(viewModel); } public IActionResult OrderSuccess() { return View(); } } // Helper class for product data public class Product { public int Id { get; set; } public string Name { get; set; } }
Step 3: Build the Razor View
The view will render order header fields, initial line items, and include JavaScript to add/remove lines dynamically. The critical part is using field names that ASP.NET Core's model binder can recognize (like LineItems[0].ProductId):
@model OrderViewModel @{ ViewData["Title"] = "Create New Order"; } <h1>Create Order</h1> <form asp-action="Create" method="post"> <!-- Order Header Section --> <div class="mb-3"> <label asp-for="Header.CustomerName" class="form-label"></label> <input asp-for="Header.CustomerName" class="form-control" /> <span asp-validation-for="Header.CustomerName" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="Header.CustomerEmail" class="form-label"></label> <input asp-for="Header.CustomerEmail" class="form-control" /> <span asp-validation-for="Header.CustomerEmail" class="text-danger"></span> </div> <div class="mb-3"> <label asp-for="Header.OrderDate" class="form-label"></label> <input asp-for="Header.OrderDate" class="form-control" type="date" /> <span asp-validation-for="Header.OrderDate" class="text-danger"></span> </div> <!-- Line Items Section --> <h3>Order Items</h3> <div id="lineItemsContainer"> @for (int i = 0; i < Model.LineItems.Count; i++) { <div class="line-item row mb-3" data-index="@i"> <div class="col-md-6"> <label class="form-label">Product</label> <select asp-for="LineItems[i].ProductId" asp-items="@ViewBag.Products" class="form-control"> <option value="">Select a product</option> </select> <span asp-validation-for="LineItems[i].ProductId" class="text-danger"></span> </div> <div class="col-md-4"> <label class="form-label">Quantity</label> <input asp-for="LineItems[i].Quantity" class="form-control" min="1" /> <span asp-validation-for="LineItems[i].Quantity" class="text-danger"></span> </div> <div class="col-md-2 align-self-end"> <button type="button" class="btn btn-danger remove-line-item">Remove</button> </div> </div> } </div> <!-- Action Buttons --> <button type="button" id="addLineItem" class="btn btn-primary mb-3">Add Line Item</button> <button type="submit" class="btn btn-success">Submit Order</button> </form> <!-- Hidden Template for New Line Items --> <div id="lineItemTemplate" style="display: none;"> <div class="line-item row mb-3" data-index="__INDEX__"> <div class="col-md-6"> <label class="form-label">Product</label> <select name="LineItems[__INDEX__].ProductId" asp-items="@ViewBag.Products" class="form-control"> <option value="">Select a product</option> </select> <span class="text-danger field-validation-valid" data-valmsg-for="LineItems[__INDEX__].ProductId" data-valmsg-replace="true"></span> </div> <div class="col-md-4"> <label class="form-label">Quantity</label> <input name="LineItems[__INDEX__].Quantity" type="number" min="1" class="form-control" value="1" /> <span class="text-danger field-validation-valid" data-valmsg-for="LineItems[__INDEX__].Quantity" data-valmsg-replace="true"></span> </div> <div class="col-md-2 align-self-end"> <button type="button" class="btn btn-danger remove-line-item">Remove</button> </div> </div> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} <script> document.addEventListener('DOMContentLoaded', function() { const addButton = document.getElementById('addLineItem'); const container = document.getElementById('lineItemsContainer'); const template = document.getElementById('lineItemTemplate').innerHTML; // Add new line item addButton.addEventListener('click', function() { const nextIndex = document.querySelectorAll('.line-item').length; const newLine = template.replace(/__INDEX__/g, nextIndex); container.insertAdjacentHTML('beforeend', newLine); }); // Remove line item (event delegation for dynamic elements) container.addEventListener('click', function(e) { if (e.target.classList.contains('remove-line-item')) { e.target.closest('.line-item').remove(); // Optional: Reindex items to clean up numbering (works without this, but looks neater) reindexLineItems(); } }); // Reindex line items after removal function reindexLineItems() { document.querySelectorAll('.line-item').forEach((item, index) => { item.setAttribute('data-index', index); // Update field names for model binding item.querySelector('select').name = `LineItems[${index}].ProductId`; item.querySelector('input').name = `LineItems[${index}].Quantity`; // Update validation message attributes const validationSpans = item.querySelectorAll('.field-validation-valid'); validationSpans[0].setAttribute('data-valmsg-for', `LineItems[${index}].ProductId`); validationSpans[1].setAttribute('data-valmsg-for', `LineItems[${index}].Quantity`); }); } }); </script> }
Key Things to Note
- Model Binding: The field name pattern
LineItems[N].PropertyNamelets ASP.NET Core automatically map form data to theList<OrderLineItem>in your view model. - Dynamic Lines: We use a hidden template to clone new line items, replacing the
__INDEX__placeholder with the correct number each time. - Validation: Client-side validation works for both initial and dynamically added lines, thanks to updating validation attributes during reindexing.
- Remove Functionality: Event delegation ensures remove buttons work for lines added after page load.
This setup will give you a fully functional order form that supports dynamic line items and proper form submission.
内容的提问来源于stack exchange,提问作者Scott

