zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    关于返回上一页功能
    Mybatis Update statement Date null
    SQLite reset password
    Bootstrap Validator使用特性,动态(Dynamic)添加的input的验证问题
    Eclipse使用Maven2的一次环境清理记录
    Server Tomcat v7.0 Server at localhost failed to start
    PowerShell一例
    Server Tomcat v7.0 Server at libra failed to start
    商标注册英语
    A glance for agile method
  • 原文地址:https://www.cnblogs.com/morang/p/7593215.html
Copyright © 2011-2022 走看看