ASP.NET Core 6.0 MVC如何实现嵌套集合绑定?
在ASP.NET Core 6.0 MVC中绑定嵌套集合的解决方案
你之前在ASP.NET 4.6.2中使用的BeginCollectionItem扩展依赖于System.Web相关API,在ASP.NET Core中这些API已被移除,导致原有代码失效。下面是适配ASP.NET Core 6.0 MVC的实现方案:
一、适配ASP.NET Core的BeginCollectionItem扩展
替换原有依赖System.Web的扩展方法,改用ASP.NET Core原生API实现:
using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Home.DPS.Web.Extensions { public static class HtmlExtensions { private const string JQueryTemplatingEnabledKey = "__BeginCollectionItem_jQuery"; /// <summary> /// 为集合项生成索引字段,并设置字段前缀,支持模板化和回发时的模型状态保持 /// </summary> public static IDisposable BeginCollectionItem<TModel>(this IHtmlHelper<TModel> html, string collectionName) { if (string.IsNullOrEmpty(collectionName)) throw new ArgumentException("collectionName不能为空", nameof(collectionName)); var collectionIndexFieldName = $"{collectionName}.Index"; string itemIndex = null; if (html.ViewData.ContainsKey(JQueryTemplatingEnabledKey)) { itemIndex = "${index}"; } else { itemIndex = GetCollectionItemIndex(html.ViewContext.HttpContext, collectionIndexFieldName); } var collectionItemName = $"{collectionName}[{itemIndex}]"; var indexField = new TagBuilder("input"); indexField.MergeAttributes(new Dictionary<string, string> { { "name", collectionIndexFieldName }, { "value", itemIndex }, { "type", "hidden" }, { "autocomplete", "off" } }); html.ViewContext.Writer.Write(indexField.ToString(TagRenderMode.SelfClosing)); var prefix = html.ViewData.TemplateInfo.HtmlFieldPrefix; html.ViewData.TemplateInfo.HtmlFieldPrefix = collectionItemName; return new CollectionItemScope(html.ViewData.TemplateInfo, prefix); } /// <summary> /// 生成用于动态添加项的jQuery模板 /// </summary> public static IHtmlContent CollectionItemJQueryTemplate<TModel, TCollectionItem>(this IHtmlHelper<TModel> html, string partialViewName, TCollectionItem modelDefaultValues) { var viewData = new ViewDataDictionary<TCollectionItem>(html.ViewData, modelDefaultValues); viewData.Add(JQueryTemplatingEnabledKey, true); return html.Partial(partialViewName, modelDefaultValues, viewData); } /// <summary> /// 从请求中复用已有的索引值,保持模型状态一致;没有则生成新的Guid索引 /// </summary> private static string GetCollectionItemIndex(HttpContext context, string collectionIndexFieldName) { var previousIndices = context.Items.TryGetValue(collectionIndexFieldName, out var value) ? value as Queue<string> : new Queue<string>(); if (!context.Items.ContainsKey(collectionIndexFieldName)) { context.Items[collectionIndexFieldName] = previousIndices; var previousIndicesValues = context.Request.Form[collectionIndexFieldName]; if (!string.IsNullOrWhiteSpace(previousIndicesValues)) { foreach (var index in previousIndicesValues.ToString().Split(',')) previousIndices.Enqueue(index); } } return previousIndices.Count > 0 ? previousIndices.Dequeue() : Guid.NewGuid().ToString(); } private class CollectionItemScope : IDisposable { private readonly TemplateInfo _templateInfo; private readonly string _previousPrefix; public CollectionItemScope(TemplateInfo templateInfo, string previousPrefix) { _templateInfo = templateInfo; _previousPrefix = previousPrefix; } public void Dispose() { _templateInfo.HtmlFieldPrefix = _previousPrefix; } } } }
二、修改分部视图(_TransactionFilter.cshtml)
使用ASP.NET Core Tag Helper替代旧的HtmlHelper方法,简化代码并提升兼容性:
@using Home.DPS.Web.Extensions @model Home.DPS.Domain.Models.TransactionFilterDto <li> @using (Html.BeginCollectionItem("TransactionFilters")) { <div class="form-row align-items-center"> <input type="hidden" asp-for="Id" /> <div class="col-2"> <input asp-for="Name" class="form-control form-control-sm" placeholder="名称" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="col-2 my-1"> <select asp-for="PaymentFrequency" class="form-control form-control-sm"> <option value="">选择频率</option> @foreach (var enumOption in Html.GetEnumSelectList<PaymentFrequency>()) { <option value="@enumOption.Value">@enumOption.Text</option> } </select> <span asp-validation-for="PaymentFrequency" class="text-danger"></span> </div> <div class="col-7"> <div class="input-group input-group-sm"> <div class="input-group-prepend"> <div class="input-group-text">$</div> </div> <input asp-for="Regex" class="form-control form-control-sm" placeholder="正则表达式" /> <span asp-validation-for="Regex" class="text-danger"></span> </div> </div> <div class="col-1"> <a href="#" onclick="$(this).closest('li').remove();"><i class="fa fa-trash"></i></a> </div> </div> } </li>
三、修改主视图(Create.cshtml)
添加动态生成新项的JavaScript逻辑,使用异步渲染方法适配Core:
<div class="card top-buffer-md"> <details open> <summary class="card-header bg-secondary text-white">交易过滤器</summary> <div class="card-body"> <fieldset class="top-buffer-md"> <ul id="transactionfilter-container" style="list-style-type: none"> @if (Model.TransactionFilters != null) { foreach (var filter in Model.TransactionFilters) { await Html.RenderPartialAsync("_TransactionFilter", filter); } } </ul> <a id="add-ag" href="#">添加交易过滤器</a> <!-- 隐藏的模板容器 --> <div id="transaction-filter-template" style="display:none"> @Html.CollectionItemJQueryTemplate("_TransactionFilter", new TransactionFilterDto()) </div> </fieldset> </div> </details> </div> @section Scripts { <script> $(document).ready(function() { $("#add-ag").click(function(e) { e.preventDefault(); var template = $("#transaction-filter-template").html(); // 将模板中的占位符替换为新的Guid索引 var newItem = template.replace(/\$\{index\}/g, generateGuid()); $("#transactionfilter-container").append(newItem); // 重新初始化表单验证 $("form").validate(); }); // 生成Guid字符串 function generateGuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } }); </script> }
四、控制器接收模型
控制器Action无需修改,直接接收视图模型即可,Core的模型绑定会自动处理嵌套集合:
[HttpPost] public IActionResult Create(ApplicationSettingsDto model) { if (ModelState.IsValid) { // 执行保存逻辑 return RedirectToAction("Index"); } return View(model); }
核心说明
- ASP.NET Core MVC支持非连续索引的集合绑定,只要每个集合项对应唯一的
[collectionName].Index隐藏字段,模型绑定即可正确识别。 - 移除了对System.Web的依赖,改用ASP.NET Core原生的
HttpContext和IHtmlHelperAPI,彻底解决兼容性问题。 - 使用Tag Helper提升视图代码的简洁性和类型安全性,同时获得更好的开发体验。
内容的提问来源于stack exchange,提问作者Kieran
相关产品推荐
相关产品推荐

