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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM