• mvc一对多模型表单的快速构建


    功能需求描述

    Q:在实际的开发中,经常会遇到一个模型中包含有多个条目的表单。如何将数据提交到后台?
    A: 以数组的形式提交到后台就Ok了(真的那么简单么,如果再嵌套一层呢?)
    A2:拆分多个模型,映射就没啥问题了。但......有点麻烦啊~~

    接下来说说如何将下面的模型提交到后台

        /// <summary>
        /// 计划模型
        /// </summary>
        public class PlanModel
        {
            public int Id{ get; set; }
            /// <summary>
            /// 计划名称
            /// </summary>
            public string PlanName { get; set; }
            /// <summary>
            /// 描述
            /// </summary>
            public string Remark { get; set; }
            /// <summary>
            /// 方案集合
            /// </summary>
            public List<CaseModel> Cases { get; set; }
        }
        /// <summary>
        /// 方案模型
        /// </summary>
        public class CaseModel
        {
            public int Id{ get; set; }
            /// <summary>
            /// 标题
            /// </summary>
            public string Title { get; set; }
            /// <summary>
            /// 描述
            /// </summary>
            public string Description { get; set; }
            /// <summary>
            /// 作者
            /// </summary>
            public string Author { get; set; }
    
        }
    

    根据此模型,编辑的页面会如下图所示,一些基本信息加上可增可减的条目
    图片

    实现效果

    图片

    如何实现这个功能(asp.net mvc)

    1. 新建视图页面(略)
    2. 条目的显示增加删除

    控制器代码

        public class HomeController : Controller
        {
            [HttpGet]
            public ActionResult Index()
            {
                var model = new PlanModel() {};
                return View(model);
            }
            public ActionResult CaseRow()
            {
                return View("_CaseRow", new CaseModel());
            }
            [HttpPost]
            public ActionResult Form(PlanModel model)
            {
                return Json(model);
            }
    
        }
    

    编辑页条目显示代码

     <div class="form-group">
                <label class="col-sm-3 control-label">计划方案:</label>
                <div class="col-sm-7 ">
                    <table class="table table-bordered table-condensed">
                        <thead>
                            <tr class="text-center">
                                <th class="text-center">方案名称</th>
                                <th class="text-center">方案作者</th>
                                <th class="text-left">方案描述</th>
                                <th class="text-center" width="100">
                                    <span>操作</span>
                                    <span title="添加方案" id="add_case" class="glyphicon glyphicon-plus"></span>
                                </th>
                            </tr>
                        </thead>
                        <tbody id="case_list">
                            @if (Model.Cases != null)
                            {
                                foreach (var item in Model.Cases)
                                {
                                    Html.RenderPartial("_CaseRow", item);
                                }
                            }
                        </tbody>
                    </table>
                </div>
            </div>
    

    页面增加/删按钮js代码 + 验证

    <script src="~/Scripts/jquery.validate.min.js"></script>
    <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#case_list").delegate(".del_tr", "click", function () {
                $(this).closest("tr").remove();
            });
            $("#add_case").click(function () {
                //ajax请求返回新增方案视图代码
                $.get('@Url.Action("CaseRow")', function (data) {
                    $("#case_list").append(data);
                    //重置验证模型
                    $("form").removeData("validator").removeData("unobtrusiveValidation");
                    $.validator.unobtrusive.parse($("form"));
                });
            });
    
        });
    </script>
    

    _CaseRow.cshtml分部视图代码

    若要以集合/数组的形式提交到后台,须以name[]的格式提交,所以我能想到的就是这样去写(这种方案不可取!!)
    但是这样写的话且不说太麻烦,验证也不行,一不小心也就写错了。所以这种方案并不可取

    @{ 
        Layout = null;
        KeyValuePair<string, string> keyValuePair = new KeyValuePair<string, string>("Cases", Guid.NewGuid().ToString("N"));
        var prefix = keyValuePair.Key+"["+keyValuePair.Value+"].";
    }
    @model MvcDemo.Models.CaseModel
    <tr>
            <td>
                <input type="hidden" name="@(keyValuePair.Key+".index")" value="@keyValuePair.Value"/>
                <input type="hidden" class="form-control" name="@(prefix)Id" value="@Model.Id" />
                <input type="text" class="form-control" name="@(prefix)Title" value="@Model.Title" />
            </td>
            <td>
                @Html.TextBox(prefix+nameof(Model.Author),Model.Author, new { @class = "form-control" })
            </td>
            <td>
                @Html.TextBox(prefix + nameof(Model.Description), Model.Description, new { @class = "form-control" })
            </td>
            <td class="text-center">
                <span class="del_tr glyphicon glyphicon-remove-circle"></span>
            </td>
    </tr>
    

    而后发现大神写的一个HtmlPrefixScopeExtensions扩展类,可自动生成的表单前缀标识,使用方便,也能够使用验证
    只需将表单包裹在@using (Html.BeginCollectionItem("子集合的属性名称")){}中即可,文末分享

    @{ 
        Layout = null;
    }
    @model MvcDemo.Models.CaseModel
    @using MvcDemo.Extensions
    <tr>
        @using (Html.BeginCollectionItem("Cases"))
        {
            <td>
                @Html.HiddenFor(e => e.Id)
                @Html.TextBoxFor(e => e.Title, new { @class = "form-control" })
                @Html.ValidationMessageFor(e => e.Title)
            </td>
            <td>
                @Html.TextBoxFor(e => e.Author, new { @class = "form-control" })
            </td>
            <td>
                @Html.TextBoxFor(e => e.Description, new { @class = "form-control" })
            </td>
            <td class="text-center">
                <span class="del_tr glyphicon glyphicon-remove-circle"></span>
            </td>
        }
    </tr>
    

    然后提交表单可以发现格式如下,并能取到数据
    图片

    MvcDemo.Extensions命名空间下的HtmlPrefixScopeExtensions扩展类

    命名空间自行引用

    1. asp.net mvc版本
        public static class HtmlPrefixScopeExtensions
        {
            private const string IdsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
    
            /// <summary>
            /// 
            /// </summary>
            /// <param name="html"></param>
            /// <param name="collectionName"></param>
            /// <param name="createDummyForm">是否使用虚拟表单,为了解决上下文中不存在表单,无法生成验证信息</param>
            /// <returns></returns>
            public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName,
                                                          bool createDummyForm = false, bool clientValidationEnabled = false)
            {
                if (clientValidationEnabled == true)
                    html.ViewContext.ClientValidationEnabled = true;
    
                if (createDummyForm == true)
                {
                    if (html.ViewContext != null && html.ViewContext.FormContext == null)
                    {
                        var dummyFormContext = new FormContext();
                        html.ViewContext.FormContext = dummyFormContext;
                    }
                }
    
                return BeginCollectionItem(html, collectionName, html.ViewContext.Writer);
            }
    
            private static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName, TextWriter writer)
            {
                var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
                var itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().GetHashCode().ToString("x");
    
                writer.WriteLine(
                    "<input type="hidden" name="{0}.index" autocomplete="off" value="{1}" />",
                    collectionName, html.Encode(itemIndex));
    
                return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
            }
    
            private static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
            {
                return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
            }
    
            private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
            {
                var key = IdsToReuseKey + collectionName;
                var queue = (Queue<string>)httpContext.Items[key];
                if (queue == null)
                {
                    httpContext.Items[key] = queue = new Queue<string>();
                    var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
                    if (!string.IsNullOrEmpty(previouslyUsedIds))
                        foreach (var previouslyUsedId in previouslyUsedIds.Split(','))
                            queue.Enqueue(previouslyUsedId);
                }
                return queue;
            }
    
            internal class HtmlFieldPrefixScope : IDisposable
            {
                internal readonly TemplateInfo TemplateInfo;
                internal readonly string PreviousHtmlFieldPrefix;
    
                public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
                {
                    TemplateInfo = templateInfo;
    
                    PreviousHtmlFieldPrefix = TemplateInfo.HtmlFieldPrefix;
                    TemplateInfo.HtmlFieldPrefix = htmlFieldPrefix;
    
                }
    
                public void Dispose()
                {
                    TemplateInfo.HtmlFieldPrefix = PreviousHtmlFieldPrefix;
                }
            }
        }
    
    1. asp.net core版本
        public static class HtmlPrefixScopeExtensions
        {
            private const string IdsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
    
            public static IDisposable BeginCollectionItem(this IHtmlHelper html, string collectionName)
            {
                return BeginCollectionItem(html, collectionName, html.ViewContext.Writer);
            }
    
            private static IDisposable BeginCollectionItem(this IHtmlHelper html, string collectionName, TextWriter writer)
            {
                if (html.ViewData["ContainerPrefix"] != null)
                    collectionName = string.Concat(html.ViewData["ContainerPrefix"], ".", collectionName);
    
                var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
                var itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
    
                string htmlFieldPrefix = $"{collectionName}[{itemIndex}]";
                html.ViewData["ContainerPrefix"] = htmlFieldPrefix;
    
                /* 
                 * html.Name(); has been removed
                 * because of incorrect naming of collection items
                 * e.g.
                 * let collectionName = "Collection"
                 * the first item's name was Collection[0].Collection[<GUID>]
                 * instead of Collection[<GUID>]
                 */
                string indexInputName = $"{collectionName}.index";
    
                // autocomplete="off" is needed to work around a very annoying Chrome behaviour
                // whereby it reuses old values after the user clicks "Back", which causes the
                // xyz.index and xyz[...] values to get out of sync.
                writer.WriteLine($@"<input type=""hidden"" name=""{indexInputName}"" autocomplete=""off"" value=""{html.Encode(itemIndex)}"" />");
    
    
                return BeginHtmlFieldPrefixScope(html, htmlFieldPrefix);
            }
    
            private static IDisposable BeginHtmlFieldPrefixScope(this IHtmlHelper html, string htmlFieldPrefix)
            {
                return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
            }
    
            private static Queue<string> GetIdsToReuse(HttpContext httpContext, string collectionName)
            {
                // We need to use the same sequence of IDs following a server-side validation failure,
                // otherwise the framework won't render the validation error messages next to each item.
                var key = IdsToReuseKey + collectionName;
                var queue = (Queue<string>)httpContext.Items[key];
                if (queue == null)
                {
                    httpContext.Items[key] = queue = new Queue<string>();
    
                    if (httpContext.Request.Method == "POST" && httpContext.Request.HasFormContentType)
                    {
                        StringValues previouslyUsedIds = httpContext.Request.Form[collectionName + ".index"];
                        if (!string.IsNullOrEmpty(previouslyUsedIds))
                            foreach (var previouslyUsedId in previouslyUsedIds)
                                queue.Enqueue(previouslyUsedId);
                    }
                }
                return queue;
            }
    
            internal class HtmlFieldPrefixScope : IDisposable
            {
                internal readonly TemplateInfo TemplateInfo;
                internal readonly string PreviousHtmlFieldPrefix;
    
                public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
                {
                    TemplateInfo = templateInfo;
    
                    PreviousHtmlFieldPrefix = TemplateInfo.HtmlFieldPrefix;
                    TemplateInfo.HtmlFieldPrefix = htmlFieldPrefix;
                }
    
                public void Dispose()
                {
                    TemplateInfo.HtmlFieldPrefix = PreviousHtmlFieldPrefix;
                }
            }
        }
    

    命名空间自行引用~~

    End

    完整源码:https://coding.net/u/yimocoding/p/WeDemo/git/tree/MvcFormExt/MvcFormExt/MvcDemo

  • 相关阅读:
    RAC连接时的2种方式Connect Time Failver和taf
    ElasticSearch Root身份运行
    sql in按照指定顺序排序
    JAVA字符串格式化-String.format()的使用
    java.lang.IllegalArgumentException: An invalid domain [.test.com] was specified for this cookie
    全文搜索引擎 Elasticsearch 入门教程
    监控页面后退前进,浏览器文档加载事件之pageshow、pagehide
    ios 上浏览器返回上一页不会刷新页面问题,页面初始化的方法不执行
    SQL之case when then用法
    MySQL表的四种分区类型
  • 原文地址:https://www.cnblogs.com/morang/p/7593215.html
Copyright © 2020-2023  润新知