ASP.NET MVC 5 列表编辑器结合 Bootstrap 模态框






4.84/5 (53投票s)
在此示例中,我将尝试演示如何在单个视图中实现一对多模型的编辑。
引言
我们的想法是显示标准的编辑视图,其中包含主对象的标准脚手架编辑器,并在此基础上添加编辑器列表,允许在同一页面上编辑子对象列表。
背景
通常,我们从 VS2013 Asp.Net MVC 模板和脚手架中获得的是针对简单模型的开箱即用功能。在创建了几个项目后,我决定创建一个发票示例。
我立即发现这不是一项简单的任务,直到我发现了神奇的 RenderAction Http 扩展。我试图让附加的示例非常简单易懂,但需要具备 Asp.Net MVC 的基本知识。
代码
首先,让我们创建一个名为 TestAjax 的标准新 Asp.Net 项目,选择 MVC 模板,核心引用设置为 MVC,并且不启用身份验证。
为了简单起见,我们创建两个模型:Person(人)和一个分配给每个 Person 的 Address(地址)列表。
在 Models 文件夹中添加两个类。
namespace TestAjax.Models { public class Person { public int Id { get; set; } [Display(Name = "First Name")] [Required] [StringLength(255, MinimumLength = 3)] public string Name { get; set; } [Display(Name = "Last Name")] [Required] [StringLength(255, MinimumLength = 3)] public string Surname { get; set; } public virtual ICollection<Address> Addresses { get; set; } } }
namespace TestAjax.Models { public class Address { public int Id { get; set; } [Required] [StringLength(255, MinimumLength = 3)] public string City { get; set; } [Display(Name = "Street Address")] public string Street { get; set; } [Phone] public string Phone { get; set; } public int PersonID { get; set; } public virtual Person Person { get; set; } } }
现在,模型已就位,我们来创建控制器。右键单击 Controllers 文件夹,选择 Add(添加),Controller... 菜单,然后在对话框中选择“MVC 5 Controller with views, using Entity Framework”(使用 Entity Framework 的带有视图的 MVC 5 控制器)。选择 Person 作为模型类,创建一个新的 DataDb context,并在对话框中设置所有复选框。重新生成项目,然后为 Address 创建另一个控制器,选择已创建的 DataDb context。
为了结束准备阶段,请删除 HomeController 和 Views 文件夹下的 Home 文件夹。在共享的 _Layout 视图中,我们将 Home、About、Contact 的标准菜单项替换为指向 People 的菜单:@Html.ActionLink("People", "Index", "People"),最后,在 App_Start\RouteConfig.cs 中,我们必须将 Home 替换为 People,这样我们的应用程序默认将指向 People 控制器。
现在,项目已启动并运行,我们将修改其呈现。
按钮中的 Bootstrap 图标
为了美化示例中的按钮,我决定使用 bootstrap glyphicons 来装饰它们。
由于 glyphicons 的语法,
<button type="button" class="btn btn-default btn-lg"> <span class="glyphicon glyphicon-star"></span> Star </button>
我添加了一个额外的辅助函数,以从 ActionLink 辅助函数生成正确的 HTML 输出。
在项目中,让我们创建一个 Helpers 文件夹,并在其中创建一个 MyHelper.cs 类。
辅助函数代码
// As the text the: "<span class='glyphicon glyphicon-plus'></span>" can be entered public static MvcHtmlString NoEncodeActionLink(this HtmlHelper htmlHelper, string text, string title, string action, string controller, object routeValues = null, object htmlAttributes = null) { UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext); TagBuilder builder = new TagBuilder("a"); builder.InnerHtml = text; builder.Attributes["title"] = title; builder.Attributes["href"] = urlHelper.Action(action, controller, routeValues); builder.MergeAttributes(new RouteValueDictionary(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); return MvcHtmlString.Create(builder.ToString()); }
因此,现在要使用 ActionLink 输出带图标的按钮,我可以使用这种语法:
@Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add new Person", "Create", "People", routeValues: null, htmlAttributes: new { @class = "btn btn-primary" })
在 People 控制器的 Index 视图中,让我们添加@usingTestAjax.Helpers指令到页面顶部。然后,让我们用以下内容替换@Html.ActionLink("Create New", "Create") 用
<div class="pull-right"> @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add new Person", "Create", "People", routeValues: null, htmlAttributes: new { @class = "btn btn-primary" }) </div>
以及 Edit、Details、Create 的 ActionLinks:
<div class="pull-right"> @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-pencil'></span>", "Edit", "Edit", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" }) @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-search'></span>", "Details", "Details", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" }) @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-trash'></span>", "Delete", "Delete", "People", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-danger" }) </div>
现在我们的图标应该就位了。
列表
为了在 Person 的 Edit 视图中开发地址列表,我将尝试实现以下想法:
Html.RenderAction 将从不同的操作注入输出到视图。我们必须记住的一点是,子控制器不能“逃离”视图。因此,我们将所有子视图都实现为通过模态框进行就地编辑的 Partial。
首先,编辑 People 控制器的 Edit 视图。就在“Back to list”div 之前,让我们添加以下部分:
<div class="row"> <div class="col-md-offset-2 col-md-10"> @{ Html.RenderAction("Index", "Addresses", new { id = Model.Id }); } </div> </div>
现在我们必须修改 Addresses Controller,因为它在 Index 操作中返回标准视图,而我们需要的是 PartialView。所以我尝试为 Index 操作输入这样的代码:
// GET: Addresses [ChildActionOnly] public async Task<ActionResult> Index(int id) { ViewBag.PersonID = id; var addresses = db.Addresses.Where(a => a.PersonID == id); return PartialView("_Index", await addresses.ToListAsync()); }
启动后,我收到了一个错误“HttpServerUtility.Execute was blocked by waiting for the end of an asynchronous operation.”。在网上稍作研究后,我发现异步操作在此版本的 MVC 中仍然不允许在子操作中使用。因此,作为快速修复,让我们删除 Addresses Controller 和 Views 文件夹下的 Addresses 文件夹,然后再次脚手架化 Addresses Controller,但这次取消选中“Use async controller actions”(使用异步控制器操作)的选项。编辑后的 Index 操作现在应该看起来像:
[ChildActionOnly] public ActionResult Index(int id) { ViewBag.PersonID = id; var addresses = db.Addresses.Where(a => a.PersonID == id); return PartialView("_Index", addresses.ToList()); }
然后,我们需要将 Views\Addresses 中的“Index.cshtml”重命名为“_Index.cshtml”,然后就可以运行了。我用[ChildActionOnly]属性来装饰 Index 操作,因为我们不希望直接调用此方法。id 参数允许我们检索给定人员的地址。ViewBag.PersonID 将有助于稍后在 Index 中创建地址。
程序现在正在运行,但我们还没有实现地址的就地编辑,所以点击 Create 链接会带我们到另一个视图 - 你可以暂时移除 [ChildActionOnly] 属性并点击该链接来尝试。为了避免逃离视图,我们将使用 Bootstrap Modals 配合一些 JavaScript。
Bootstrap 模态框
我不是 JavaScript 专家,但不知怎的,我开发了以下脚本来显示 bootstrap modals。
// modalform.js $(function () { $.ajaxSetup({ cache: false }); $("a[data-modal]").on("click", function (e) { // hide dropdown if any $(e.target).closest('.btn-group').children('.dropdown-toggle').dropdown('toggle'); $('#myModalContent').load(this.href, function () { $('#myModal').modal({ /*backdrop: 'static',*/ keyboard: true }, 'show'); bindForm(this); }); return false; }); }); function bindForm(dialog) { $('form', dialog).submit(function () { $.ajax({ url: this.action, type: this.method, data: $(this).serialize(), success: function (result) { if (result.success) { $('#myModal').modal('hide'); //Refresh location.reload(); } else { $('#myModalContent').html(result); bindForm(dialog); } } }); return false; }); }
脚本要正常工作需要三样东西:
1. 在视图中,这样的占位符:
<!-- modal placeholder--> <div id='myModal' class='modal fade in'> <div class="modal-dialog"> <div class="modal-content"> <div id='myModalContent'></div> </div> </div> </div>
2. 在想要放入模态框的视图中,这样的标记:
<div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Add new Address</h4> </div> @using (Html.BeginForm()) { <div class="modal-body"> // Place Form editors here </div> </div> <div class="modal-footer"> <button class="btn" data-dismiss="modal">Cancel</button> <input class="btn btn-primary" type="submit" value="Add" /> </div> }
3. 支持模态框的操作视图必须返回:Json(new{ success =true}); 而不是 View();
注意:请注意,脚本需要调用 ActionLink 中的 data-modal 属性。
注意:最好为 data-dismiss 按钮添加type="button"属性 - 这样可以避免用户按 Enter 键接受对话框时 Bootstrap Modal 出现问题 - 在某些浏览器中,没有此属性的 Modal 会直接关闭。
有关更完整的、专注于 Bootstrap 3.1.1 Modals in MVC 5 的示例,请 访问此网站。
在我们的项目中,我将 modalform.js 添加到 \Sripts 文件夹,并在 AppStart\BundleConfig.cs 文件中添加了以下行:
bundles.Add(new ScriptBundle("~/bundles/modalform").Include("~/Scripts/modalform.js"));
到 AppStart\BundleConfig.cs 文件中。
现在,编辑 "_Index.schtml" 以获得此布局:
@using TestAjax.Helpers @model IEnumerable<TestAjax.Models.Address> <!-- modal placeholder--> <div id='myModal' class='modal fade in'> <div class="modal-dialog"> <div class="modal-content"> <div id='myModalContent'></div> </div> </div> </div> <div class="panel panel-default"> <div class="panel-heading"> <strong>Address List</strong> </div> <table class="table table-hover"> <tr> <th> @Html.DisplayNameFor(model => model.City) </th> <th> @Html.DisplayNameFor(model => model.Street) </th> <th> @Html.DisplayNameFor(model => model.Phone) </th> <th>@Html.NoEncodeActionLink("<span class='glyphicon glyphicon-plus'></span>", "Add", "Create", "Addresses", routeValues: new { PersonId = ViewBag.PersonID }, htmlAttributes: new { data_modal = "", @class = "btn btn-primary pull-right" })</th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.City) </td> <td> @Html.DisplayFor(modelItem => item.Street) </td> <td> @Html.DisplayFor(modelItem => item.Phone) </td> <td> <div class="pull-right"> @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-pencil'></span>", "Edit", "Edit", "Addresses", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-default" }) @Html.NoEncodeActionLink("<span class='glyphicon glyphicon-trash'></span>", "Delete", "Delete", "Addresses", routeValues: new { id = item.Id }, htmlAttributes: new { data_modal = "", @class = "btn btn-danger" }) </div> </td> </tr> } </table> </div>
在 People 控制器的 "Edit.cshtml" 的脚本部分添加对模态框的支持。
@section Scripts { @Scripts.Render("~/bundles/jqueryval") @Scripts.Render("~/bundles/modalform") }
为了使用模态框,我们还必须修改 Addresses Controller 中的 Create 方法:
public ActionResult Create(int PersonID) { Address address = new Address(); address.PersonID = PersonID; return PartialView("_Create", address); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "Id,City,Street,Phone,PersonID")] Address address) { if (ModelState.IsValid) { db.Addresses.Add(address); db.SaveChanges(); return Json(new { success = true }); } return PartialView("_Create"); }
最后,重命名后的 Addresses 的 "_Create.cshtml" 应该具有这样的形状:
@model TestAjax.Models.Address <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">Add new Address</h4> </div> @using (Html.BeginForm()) { <div class="modal-body"> @Html.AntiForgeryToken() <div class="form-horizontal"> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div class="form-group"> @Html.LabelFor(model => model.City, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.City, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.City, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Street, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Street, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Street, "", new { @class = "text-danger" }) </div> </div> <div class="form-group"> @Html.LabelFor(model => model.Phone, htmlAttributes: new { @class = "control-label col-md-2" }) <div class="col-md-10"> @Html.EditorFor(model => model.Phone, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Phone, "", new { @class = "text-danger" }) </div> </div> </div> </div> <div class="modal-footer"> <button class="btn" data-dismiss="modal">Cancel</button> <input class="btn btn-primary" type="submit" value="Add" /> </div> }
现在,我们可以在不离开 Person Edit 视图的情况下添加新的 Address 行。
相同的模式应该用于创建 _Edit 和 _Delete Partial。
此外,我们可以创建一个 _List Partial 来显示在 Person 的 Details 视图中。
有关其余 Partial 的信息,请参阅附件下载,其中包含项目的完整源代码。
注意:重新生成项目以更新附件中的 Nuget 包。您应该在 Visual Studio 的 NuGet 设置中勾选“Allow Nuget to download missing packages”(允许 Nuget 下载缺失的包)和“Automatically check for missing packages during build”(构建时自动检查缺失的包)这两个选项。
问题
提出的解决方案有一个缺陷:
每次添加、更改或删除 Address 时,People Edit 视图都会刷新。
解决方案
解决上述问题的方案是**Ajaxify**它。
全页刷新来自我脚本中的这行:
location.reload();
为了修复它,我们必须准备一个新版本:
// modalform.js $(function () { $.ajaxSetup({ cache: false }); $("a[data-modal]").on("click", function (e) { // hide dropdown if any (this is used wehen invoking modal from link in bootstrap dropdown ) //$(e.target).closest('.btn-group').children('.dropdown-toggle').dropdown('toggle'); $('#myModalContent').load(this.href, function () { $('#myModal').modal({ /*backdrop: 'static',*/ keyboard: true }, 'show'); bindForm(this); }); return false; }); }); function bindForm(dialog) { $('form', dialog).submit(function () { $.ajax({ url: this.action, type: this.method, data: $(this).serialize(), success: function (result) { if (result.success) { $('#myModal').modal('hide'); $('#replacetarget').load(result.url); // Load data from the server and place the returned HTML into the matched element } else { $('#myModalContent').html(result); bindForm(dialog); } } }); return false; }); }
正如你所见,现在我们正在用id="replacetarget"使用从 Action 收到的 JSON 中的 url 来替换 DOM 元素的内容。
我们的项目需要进行一些重构才能使脚本正常工作。
首先,让我们将@脚本.Render("~/bundles/jquery")行从 Shared\_Layout.cshtml 移动到页面顶部的
区域。原因是我们将需要比以前更早地访问 jQuery。现在,在 People 控制器的 Edit 视图中,让我们修改表单结束括号后的部分:
} // End of @using (Html.BeginForm()) <!-- modal placeholder--> <div id='myModal' class='modal fade in'> <div class="modal-dialog"> <div class="modal-content"> <div id='myModalContent'></div> </div> </div> </div> <div class="row"> <div class="col-md-offset-2 col-md-10" id="replacetarget"> @{ Html.RenderAction("Index", "Addresses", new { id = Model.Id }); } </div> </div> <p> @Html.ActionLink("Back to List", "Index") </p> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
您可以看到,我们将模态框占位符移到了此处(已从 _Index partial 中移除),因为我们不希望这部分被脚本替换。就在 Html.RenderAction 之前,我添加了id="replacetarget"到 div。这是将被脚本替换的 div。脚本部分现在只包含验证。
在 _Index Partial 中,如上所述移除模态框块,并在页面末尾添加 @Scripts.Render("~/bundles/modalform")。在 标签内添加 modalform.js 的内容(因为 @Script.Render 在 Partial 中不起作用)。这就是为什么我们需要提前加载 jQuery。modalform 脚本放在这里是因为所有的
我们必须提供的最后一项是脚本中使用的 result.url JSON 数据。
深入 Addresses Controller 代码,并替换每个
returnJson(new{ success =true});
行用
字符串url = Url.Action("Index", "Addresses", new{ id = address.PersonID });
returnJson(new{ success =true, url = url });
我们在这里使用 Url.Action 辅助函数来使用我们当前正在编辑的 PersonID 构建正确的 URL。
最后,移除[ChildActionOnly] 属性,因为 Index 方法现在将由脚本调用,而不仅仅是 Person 的 Edit 视图中的 RenderAction。
与之前一样,代码可在此上方下载。
关注点
从下载中您可以了解到:
- 如何处理视图中的就地动态列表。
- 如何在 Bootstrap Modals 中显示表单。
- 如何使用 Bootstrap glyph icons 装饰 Bootstrap 按钮。
- 如何突出显示 Bootstrap 活动导航栏菜单项。
- 如何使用 Ajax 只刷新页面的一部分。
历史
2015-02-15 添加了两个小的修复。
问题:模态对话框有时不再是模态了。感谢 agpulidoc 在评论中提供的解决方案。
修复:Partials 不支持 @script 区域 - 直接替换为 script 或引入扩展。
问题:当验证在模态框中报告错误时,Submit Save Person 不工作 - 在评论中发现。
修复:在模态框脚本中,当 !result.success 时 - 应该调用 bindForm(dialog); 而不是 bindForm();
2014-01-17 添加了 Ajax 部分。
2014-01-15 已修订。
2014-01-14 初始版本。