You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 07:37:00