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; }
- }
根据此模型,编辑的页面会如下图所示,一些基本信息加上可增可减的条目
- 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>
若要以集合/数组的形式提交到后台,须以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>
然后提交表单可以发现格式如下,并能取到数据
命名空间自行引用
- 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;
- }
- }
- }
命名空间自行引用~~
- 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;
- }
- }
- }
完整源码:https://coding.net/u/yimocoding/p/WeDemo/git/tree/MvcFormExt/MvcFormExt/MvcDemo
来源: http://www.cnblogs.com/morang/p/7593215.html