ASP.NET MVC如何同时绑定对象与数组并提交至控制器?
问题分析与ASP.NET MVC原生解决方案
现有方案的问题根源
- 415不支持的媒体类型错误:控制器方法同时接受表单绑定的
PaymentCondition和[FromBody]标记的列表,ASP.NET无法同时解析两种请求格式(表单为application/x-www-form-urlencoded,JSON为application/json),且你的AJAX仅发送了列表数据,未传递PaymentCondition字段,导致参数绑定失败。 - 两次请求:
<input type="submit">点击时会先触发onclick中的AJAX请求,随后浏览器默认触发表单提交,因此产生两次请求。
方案一:原生表单+ViewModel(非异步提交)
这是ASP.NET MVC最原生的实现方式,通过ViewModel封装主模型与列表数据,利用自动模型绑定完成提交,无需额外AJAX代码。
1. 定义ViewModel
将主模型和列表数据封装到同一个ViewModel中:
public class PaymentConditionWithInstalmentsViewModel { public string Name { get; set; } public decimal Fee { get; set; } public decimal Discount { get; set; } public decimal Fine { get; set; } public int? Id { get; set; } public List<InstalmentDTO> Instalments { get; set; } = new List<InstalmentDTO>(); } public class InstalmentDTO { public int Number { get; set; } public int Days { get; set; } public decimal Percentage { get; set; } public int PaymentMethodId { get; set; } }
2. 控制器代码
移除[FromBody]标记,直接接收ViewModel参数:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> CreateWithInstalment(PaymentConditionWithInstalmentsViewModel model) { if (ModelState.IsValid) { // 转换ViewModel为EF实体 var paymentCondition = new PaymentCondition { Name = model.Name, Fee = model.Fee, Discount = model.Discount, Fine = model.Fine, Id = model.Id ?? 0 }; // 处理分期付款列表 foreach (var instalmentDto in model.Instalments) { // 转换为实体并关联到paymentCondition } await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(model); }
3. 强类型视图代码
通过循环生成列表字段,ASP.NET会自动处理列表的模型绑定:
@model PaymentConditionWithInstalmentsViewModel <form asp-action="CreateWithInstalment" method="post"> <!-- 主模型字段 --> <div class="form-group"> <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Fee"></label> <input asp-for="Fee" class="form-control" /> <span asp-validation-for="Fee" class="text-danger"></span> </div> <!-- 省略Discount、Fine、Id等字段 --> <!-- 分期付款列表容器 --> <div id="instalments-container"> @for (int i = 0; i < Model.Instalments.Count; i++) { <div class="instalment-item"> <h5>分期付款 @(i+1)</h5> <input type="hidden" asp-for="Instalments[i].Number" /> <div class="form-group"> <label asp-for="Instalments[i].Days"></label> <input asp-for="Instalments[i].Days" class="form-control" /> </div> <div class="form-group"> <label asp-for="Instalments[i].Percentage"></label> <input asp-for="Instalments[i].Percentage" class="form-control" step="0.01" /> </div> <div class="form-group"> <label asp-for="Instalments[i].PaymentMethodId"></label> <select asp-for="Instalments[i].PaymentMethodId" class="form-control"></select> </div> <button type="button" class="btn btn-danger" onclick="removeInstalment(this)">删除</button> </div> } </div> <button type="button" class="btn btn-secondary" onclick="addInstalment()">添加分期付款</button> <button type="submit" class="btn btn-primary">创建</button> </form> <script> // 动态添加分期付款项 function addInstalment() { const container = document.getElementById('instalments-container'); const itemCount = container.children.length; const newItem = document.createElement('div'); newItem.className = 'instalment-item'; newItem.innerHTML = ` <h5>分期付款 ${itemCount + 1}</h5> <input type="hidden" name="Instalments[${itemCount}].Number" value="${itemCount + 1}" /> <div class="form-group"> <label>天数</label> <input type="number" name="Instalments[${itemCount}].Days" class="form-control" /> </div> <div class="form-group"> <label>百分比</label> <input type="number" step="0.01" name="Instalments[${itemCount}].Percentage" class="form-control" /> </div> <div class="form-group"> <label>支付方式ID</label> <select name="Instalments[${itemCount}].PaymentMethodId" class="form-control"></select> </div> <button type="button" class="btn btn-danger" onclick="removeInstalment(this)">删除</button> `; container.appendChild(newItem); } // 删除分期付款项并更新索引 function removeInstalment(btn) { btn.parentElement.remove(); const items = document.querySelectorAll('.instalment-item'); items.forEach((item, index) => { const inputs = item.querySelectorAll('input, select'); inputs.forEach(input => { input.name = input.name.replace(/Instalments\[\d+\]/, `Instalments[${index}]`); }); item.querySelector('h5').textContent = `分期付款 ${index + 1}`; }); } </script>
方案二:Ajax.BeginForm(异步提交)
如果需要异步提交体验,可使用ASP.NET MVC原生的Ajax.BeginForm,无需手动编写jQuery AJAX逻辑。
1. 引入依赖脚本
确保视图中引入以下脚本:
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js"></script>
2. 视图代码
替换普通表单为Ajax.BeginForm:
@model PaymentConditionWithInstalmentsViewModel @using (Ajax.BeginForm("CreateWithInstalment", "PaymentConditions", new AjaxOptions { HttpMethod = "POST", OnSuccess = "handleSubmitSuccess", OnFailure = "handleSubmitFailure" }, new { id = "paymentForm" })) { @Html.AntiForgeryToken() <!-- 主模型字段和列表字段与方案一完全一致 --> <button type="button" class="btn btn-secondary" onclick="addInstalment()">添加分期付款</button> <button type="submit" class="btn btn-primary">创建</button> } <script> // addInstalment和removeInstalment函数与方案一相同 function handleSubmitSuccess() { console.log("提交成功"); // 处理成功逻辑,如刷新页面或跳转 } function handleSubmitFailure() { console.log("提交失败"); // 处理失败逻辑,如提示错误 } </script>
3. 控制器代码
与方案一完全相同,无需修改。
内容的提问来源于stack exchange,提问作者Eduardo H Anacleto
相关产品推荐
相关产品推荐

