MVC 48.SportsSore:管理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MVC 48.SportsSore:管理相关的知识,希望对你有一定的参考价值。
作者:[美]Adam Freeman 来源:《精通ASP.NET MVC 4》
本文将继续构建 SportsStore 应用程序,为网站管理员提供一个管理产品分类的方法。本文将添加一些支持功能,包括通过产品存储库进行产品的创建、编辑和删除,以及上传产品图片并将其显示在产品旁边。
1.添加分类管理
管理条目集合的惯例,是向用户显示两种形式的页面 —— 一个列表页面和一个编辑页面。
这些页面合起来可以让用户创建、读取、更新和删除集合中的条目。这些动作统称为“CRUD”。 开发人员往往需要实现 CRUD,因此 Visual Studio 通常会设法对此提供帮助,以便生成具有 CRUD 操作动作方法的 MVC 控制器,同时也提供对这些操作进行支持的视图。
1.1 创建 CRUD 控制器
本文将创建一个新控制器来处理这些管理功能。 右击 SportsStore.WebUI 项目的 Controllers 文件夹,从弹出菜单中选择“Add Controller”,将该控制器命名为“AdminController”,模板为“空的MVC控制器”:
using SportsStore.Domain.Abstract; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } public ViewResult Index() { return View(repository.Products); } } }
1.2 创建新的布局
本例打算创建一个新的 Razor 布局,以用于 SportsStore 的管理视图。这是一个简单的布局,它提供一个单一的点,可以运用这个点形成所有管理视图。
为了创建该布局,新建布局页文件 Views/Shared/_AdminLayout.cshtml 。
正如前面所解释的那样,布局的命名约定是以一个下划线字符(_)作为首字母。微软的另一个叫做WebMatrix 的技术也使用 Razor ,它利用下划线来阻止浏览器请求布局页面。虽然 MVC 不需要这种防护,但这一约定被延用到了 MVC 应用程序。
在这个布局中,本文希望创建一个对 CSS 文件的引用,详细代码如下:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/Content/Admin.css" rel="stylesheet" type="text/css" /> <title>@ViewBag.Title</title> </head> <body> <div> @RenderBody() </div> </body> </html>
对应的 Admin.css 文件代码如下:
body, td { font-family: \'Segoe UI\', Verdana; } h1 { padding: .5em; padding-top: 0; font-weight: bold; font-size: 1.5em; border-bottom: 2px solid gray; } div#content { padding: .9em; } table.grid td, table.grid th { border-bottom: 1px dotted gray; text-align: left; } table.grid { border-collapse: collapse; width: 100%; } table.grid th.NumericCol, table.grid td.NumericCol { text-align: right; padding-right: 1em; } form { margin-bottom: 0px; } div.Message { background: gray; color: white; padding: .2em; margin-top: .25em; } .field-validation-error { color: red; display: block; } .field-validation-valid { display: none; } .input-validation-error { border: 1px solid red; background-color: #ffeeee; } .validation-summary-errors { font-weight: bold; color: red; } .validation-summary-valid { display: none; }
1.3 实现 List 视图
现在,已经创建了一个新的布局,下面可以对项目添加一个视图,用于 Admin 控制器的 Index 动作方法。新建对应的视图文件 Views/Admin/Index.cshtml 。
此处打算使用支架视图(Scaffold view),在这个视图中,Visual Studio 会为一个强类型视图所选择的类进行考察并创建一个视图,该视图含有为这个模型类型量身定制的标记(可见,所谓支架视图是让用户为视图选择一个模型类型,Visual Studio 会为这个模型创建一个含有相应标记的视图,这种视图则称为强类型视图)。谓词,从模型类列表中选择 Product ,并在“Scanffold template(支架模板)”中选择 List 。
注:当使用 List 支架时,Visual Studio 假设你要使用的是一个 IEnumerable 序列的模型视图类型,因此,用户只能从列表中选择一个单一的类。
自动生成的视图文件内容如下:
@model IEnumerable<SportsStore.Domain.Entities.Product> @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>Index</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th> @Html.DisplayNameFor(model => model.Name) </th> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.Price) </th> <th> @Html.DisplayNameFor(model => model.Category) </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Description) </td> <td> @Html.DisplayFor(modelItem => item.Price) </td> <td> @Html.DisplayFor(modelItem => item.Category) </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.ProductID }) | @Html.ActionLink("Details", "Details", new { id=item.ProductID }) | @Html.ActionLink("Delete", "Delete", new { id=item.ProductID }) </td> </tr> } </table>
Visual Studio 会考察视图模型对象的类型,并根据该模型所定义的属性,生成一些表格形式的元素。通过启动应用程序,导航到 Admin/Index 地址,可以看到视图是如何渲染的。其结果如下图所示:
这种支架视图为用户用了很好的设置工作。此时得到了 Product 类中各个属性的表格列,以及进行 CRUD 操作的链接,这些链接指向同一控制区中的各个动作方法。这些标题有些冗长,而且有些东西要与先抢创建的 CSS 联系起来。编辑这个 Index.cshtml 文件,代码如下:
@model IEnumerable<SportsStore.Domain.Entities.Product> @{ ViewBag.Title = "Admin: All Products"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h1>All Products</h1> <table class="grid"> <tr> <th>ID </th> <th>Name</th> <th class="NumericCol">Price </th> <th>Actions</th> </tr> @foreach (var item in Model) { <tr> <td>@item.ProductID </td> <td>@Html.ActionLink(item.Name, "Edit", new { item.ProductID }) </td> <td class="NumericCol">@item.Price.ToString("c")</td> <td> @using (Html.BeginForm("Delete", "Admin")) { @Html.Hidden("ProductID", item.ProductID) <input type="submit" value="Delete" />} </td> </tr> } </table> <p>@Html.ActionLink("Add a new product", "Create")</p>
现在 有了一个很好的列表页面。管理员可以看到分类中的产品,并有了进行添加、删除以及查看物品的链接和按钮。以下几节将添加一些功能以支持这些动作。
1.4 编辑产品
为了提供创建和更新特性,将添加一个产品编辑页面。此工作由两个部分:
* 显示一个让管理员能够修改产品属性值得页面
* 添加一个动作方法,它能够在递交时对这些修改进行处理
创建 Edit 动作方法
下面代码显示了添加到 AdminController 类中的 Edit 方法。这是在 Index 视图中调用 Html.ActionLink 辅助器方法时所指定的动作方法。
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } public ViewResult Index() { return View(repository.Products); } public ViewResult Edit(int productId) { Product product = repository.Products.FirstOrDefault(p=>p.ProductID==productId); return View(product); } } }
这个简单的方法找出与 productId 参数对应的 ID 的产品,并把它作为一个视图模型对象进行传递。
创建 Edit 视图
现在有了一个动作方法,于是可以为它创建一个视图以便渲染。添加对应的视图文件 Views/Admin/Edit.cshtml
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h1>Edit @Model.Name</h1> @using (Html.BeginForm()) { @Html.EditorForModel(); <input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index"); }
这个示例并未手工为每个标签和输入项编写标记,而是调用了 Html.EditorForModel 辅助器方法。这个方法要求 MVC 框架创建编辑器界面,这是通过探测其模型类型来实现的——在本例中,该模型类型时 Product 类。
要想看看这个 Edit 视图所生成的页面,可以运行应用程序,并导航到 /Admin/Index 。点击一个产品名,便会看到下图所示页面:
说实话,EditorForModel 方法很方便,但它产生的结果并不令人十分满意。此外,设计者通常不希望管理员可以看到或编辑 ProductID 属性,而且,用于 Description 的文本框太小了。
可以通过使用模型元数据为 MVC 框架提供一些指示,告诉框架如何为属性创建编辑器(这里的“属性编辑器”是指在 HTML 页面上对某一属性进行数据录入或编辑的界面)。这让用户能够将注解属性运用于这个新模型类的属性上,以影响 Html.EditorForModel 方法的输出。下面代码演示了如何在 SportsStore.Domain 项目中的 Product 类上使用元数据:
using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SportsStore.Domain.Entities { public class Product { [HiddenInput(DisplayValue = false)] public int ProductID { get; set; } public string Name { get; set; } [DataType(DataType.MultilineText)] public string Description { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
HiddenInput 注解属性告诉 MVC 框架,将该属性渲染为隐藏的表单元素,而 DataType 注解属性能够只是如何显示或编辑一个值。这个示例选择了 MultilineText 选项。HiddenInput 注解属性属于 System.Web.Mvc 命名空间,而 DataType 注解属性包含在 System.ComponentModel.DataAnnotations 命名空间中。
下图显示了运用元数据后的 Edit 页面,此时已不再能看到或编辑 ProductID 属性了,而且有了一个输入 description 的多行文本。然而,这个 UI 看上去还是不够理想。
可以用 CSS 做一些简单的改善。当 MVC 框架为每个属性创建 input 字段时,它给这些 input 赋予了不同的 CSS 的 class 值。当产看上图页面的源代码时便可以看到,为产品描述创建的 textarea 元素被赋予了 "text-box multi-line"这一 CSS 的 class 值。
<textarea name="Description" id="Description" class="text-box multi-line">A boat for one person</textarea>
其他 HTML 元素也被赋予了类似的 class 值,于是可以改善 Edit 视图的外观。添加 CSS 代码如下:
.editor-field {margin-bottom:.8em;} .editor-label {font-weight:bold;} .editor-label:after {content:":";} .text-box {width:25em;} .multi-line {height:5em;font-family:\'Segoe UI\',Verdana;}
下图显示了这些样式在 Edit 视图上所具有的效果。所渲染的视图仍然是很基本的,但它的功能已具备了管理的需要。
正如在这个例子中所看到的,EditorForModel 这样的模板视图辅助器方法所创建的页面并不总能满足开发者。
更新产品存储库
在能够处理编辑之前,还需要增强产品存储库,才能够保存所做的修改。首先,要对 IProductRepository 接口添加一个新的方法:
using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Abstract { public interface IProductRepository { IQueryable<Product> Products { get; } void SaveProduct(Product product); } }
然后,将这个方法添加到存储库的 Entity Framework 实现上,即 Concrete/EFProductRepository 类,代码如下:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System.Linq; namespace SportsStore.Domain.Concrete { public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext(); public IQueryable<Product> Products { get { return context.Products; } } public void SaveProduct(Product product) { if (product.ProductID == 0) { context.Products.Add(product); } else { Product dbEntry = context.Products.Find(product.ProductID); if (dbEntry != null) { dbEntry.Name = product.Name; dbEntry.Description = product.Description; dbEntry.Price = product.Price; dbEntry.Category = product.Category; } } context.SaveChanges(); } } }
如果 ProductID 为0,这一 SaveChanges 方法实现会将一个产品加入存储库(添加产品);否则,会将任何修改用于与数据库中已存在的物品(修改产品)。
总所周知,当接收到到 ProductID 不为 0 的 Product 参数时,需要执行更新。其实现方法是,从存储库中获取同样 ProductID 的 Product 对象,并更新每个属性,以使这些属性与参数对象吻合。
这么做是因为 Entity Framework 会对数据库创建的对象保持跟踪。传递给 SaveChanges 方法的对象是由 MVC Framework 使用默认的模型绑定器创建的,这意味着, Entity Framework 实体框架不知道任何关于参数对象的事情,因而不会对数据库运用更新。解决这一问题的方式有很多,本文采取了最简单的一个,即对 Entity Framework 已知的相应的对象进行定位,并明确地对它进行更新。
另一种办法是,可以创建一个只从存储库中获取对象的自定义模型绑定器。这可能看起来像是一种更优雅地办法,但它需要对存储库接口添加查找功能,才能通过 ProductID 值对 Product 对象进行定位。
这种办法的缺点是,为了解决一个具体实现的局限性,必须从存储库的抽象定义开始添加功能。如果将来切换了存储库实现,所冒的风险则是,新的存储技术或许不能真正支持用户所实现的这种查找功能。这可能会诱使用户使用 MVC Framework 的灵活性,来避免类似于 SaveProduct 方法中的这些变通方案,但这么做可能会违背用户应用程序的设计原则。
处理 Edit 的 POST 请求
此刻已经做好了准备,以便在 Admin 控制器中实现一个重载的 Edit 动作方法,他将在管理员点击“Save”按钮时处理 POST 请求。这个方法如下:
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } public ViewResult Index() { return View(repository.Products); } public ViewResult Edit(int productId) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); return View(product); } [HttpPost] public ActionResult Edit(Product product) { if (ModelState.IsValid) { repository.SaveProduct(product); TempData["message"] = string.Format("{0} has been saved", product.Name); return RedirectToAction("Index"); } else { //数据值有误 return View(product); } } } }
该方法通过读取 ModelState.IsValid 属性的值,检查了模型绑定器已经能够验证用户递交的数据(注:模型绑定器绑定用户递交的数据、形成 Product 对象,并将该对象传递给 Edit 方法,如果这一系列活动都正常,则 ModelState.IsValid 为真)。如果一切正常,便将这些修改保存到存储库,然后调用 Index 动作方法,让用户返回到产品列表。如果数据有问题,则再次渲染 Edit 视图,以使用户能够进行修正。
在存储库中保存了这些修改之后,用 Temp Data (临时数据)特性保存了一条消息。这是一个“键/值”字典,它类似于之前已经用过的会话数据和 View Bag(视图包)特性。与会话数据的关键差别是,Temp Data 在HTTP 请求结束时会被删除。
注意, Edit 方法返回的是 ActionResult 类型。到目前为止,此书一直使用的都是 ViewResult 类型。View Result 派生于 ActionResult ,而且它是在用户希望框架去渲染一个视图时使用的。然而,其他类型的 ActionResult 也是可用的, RedirectToAction 方法所返回的便是其中之一(即,有好几种方法都返回 ActionResult 类型, RedirectToAction 的返回类型就是 ActionResult 类型)。这里在 Edit 动作方法中用它去调用 Index 动作方法。
这种情况下不能使用 ViewBag,这是因为用户被重定向了。 ViewBag 在控制器与视图之间传递数据,但它保存数据的时间不能比当前 HTTP 请求长(注意,重定向意味着用户是跨请求的,而 ViewBag 不能用于跨请求情况下控制器与视图之间的数据传递)。读者可以使用会话数据特性,但其消息是持久的,知道它被明确地删除位置,那还不如不用它(可见会话数据占用服务器资源,且需要维护,故这里不用它)。因此,Temp Dat 特性是十分合适的。其数据被限制到一个单一用户的会话(于是用户不会看到相互的 TempData),并且将会一直保存到被读取为止。在动作方法渲染的视图中,把这些数据读给已经被重定向的用户(此时,TempData 保保持的数据也就自然消亡)。
显示确认消息
本文打算在 _AdminLayout.cshtml 布局文件中处理 TempData 存储的消息。通过在模板中处理消息,可以在任何使用此模板的视图中创建消息,而不需要创建额外的 Razor 块。下面是对此文件的修改:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/Content/Admin.css" rel="stylesheet" type="text/css" /> <title>@ViewBag.Title</title> </head> <body> <div> @if (TempData["message"] != null) { <div class="Message">@TempData["message"]</div> } @RenderBody() </div> </body> </html>
提示:像这样在模板中处理消息的好处是。在用户保存了修改之后,可以看到它显示在任何渲染页面上。此刻,消息被返回给产品列表,但可以改变此工作流程去渲染一些其他视图,而用户将仍然可以看到这些消息(只要下一个书体也使用同样的布局)。
现在有了对编辑产品进行测试的所有元素。运行此应用程序,导航到 Admin/Index ,并进行编辑,点击“Save”按钮,系统将返回到列表系统,而 TempData 消息也将被显示出来,如下图所示:
如果刷新产品列表屏幕,这条消息将会消失,这是因为 TempData 在读取它时被删除了。这是很方便的,因此此处不希望还会残留过时的消息。
添加模型验证
通常情况总是这样,需要对模型实体添加验证规则。此刻,管理员可以输入负数价格或空白产品描述,那么 SportsStore 一样会把这些数据存储到数据库中(这当然不行,所以要添加验证规则)。下面代码演示了如何把数据注解属性(Data Annotations Attributes)运用于 Product 类:
using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SportsStore.Domain.Entities { public class Product { [HiddenInput(DisplayValue = false)] public int ProductID { get; set; } [Required(ErrorMessage = "Please enter a product name")] public string Name { get; set; } [DataType(DataType.MultilineText)] [Required(ErrorMessage = "Please enter a description")] public string Description { get; set; } [Required] [Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] public decimal Price { get; set; } [Required(ErrorMessage = "Please specify a category")] public string Category { get; set; } } }
在使用 Html.EditorForModel 辅助器方法来创建 form 元素以编辑 Product 时, MVC 框架添加了显示内联的验证错误所需的所有标记和 CSS 。当编辑一个产品,而输入的数据违背了上面代码所运用的验证规则时,会出现下图所示的界面。
启用客户端验证
此时,只有当管理员把编辑递交给服务器时,才会运用数据验证。大多数 Web 用户期望,如果输入的数据有问题,要立即得到反馈。这就是 Web 开发人员经常希望执行客户端验证的原因,此时,数据在浏览器中用 javascript 进行检查。 MVC 框架可以根据运用于域模型类的数据来执行客户端验证。
这一特性是默认可用的,但它上不会生效,因为还没有添加对所需的 Javascript 库的链接。最简单的办法是在 _AdminLayout.cshtml 文件中添加这些链接,这样,客户端验证便能在使用这个布局的任何页面上起作用。修改如下:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <link href="~/Content/Admin.css" rel="stylesheet" type="text/css" /> <script src="~/Scripts/jquery-1.7.1.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <title>@ViewBag.Title</title> </head> <body> <div> @if (TempData["message"] != null) { <div class="Message">@TempData["message"]</div> } @RenderBody() </div> </body> </html>
通过这些添加,客户端验证将对管理视图生效。显示给用户的错误消息的外观是相同的,因为服务器验证所使用的 CSS 的class 也有客户端验证所使用。但得到的响应时及时的,而且不需要把请求发送到服务器。在大多数情况下,客户端验证是一个有用的特性,但如果出于某种原因不希望在客户端验证,则需要使用以下语句。
... HtmlHelper.ClientValidationEnabled = false; HtmlHelper.UnobtrusiveJavaScriptEnabled = false; ...
如果把这些语句放在一个视图或一个控制器中,那么客户端验证只针对当前动作失效。要禁用整个应用程序的客户端验证,可以再 Global.asax 的 Application_Start 方法中使用这些语句,或是把这些值运用于 Web.config 文件,使用代码如下:
<appSettings> <add key="ClientValidationEnabled" value="false" /> <add key="UnobtrusiveJavaScriptEnabled" value="false" /> </appSettings>
1.5 创建新产品
下一步将实现 Create 动作方法,这是在产品列表页面中“Add a new product”链接所指定的方法。它允许管理员把一个新物品添加到产品分类。添加创建新产品的能力只需要一个小的附件,并对应用程序做出一点小改动即可。这是精心构思 MVC 应用程序功能和适应新的一个很好的例子。首先,对 AdminController 类添加 Create 方法:
public ViewResult Create() { return View("Edit", new Product()); }
这个 Create 方法并不渲染它的默认视图,而只是指明应该使用 Edit 视图。让一个动作方法去使用一个通常与另一个视图关联的视图是完全可以的。这里注入了一个新的 Product 对象作为视图模型,以便 Edit 视图用空字段进行填充。
这使用户能够修改这个空白的 Product 对象。通常,用户期望一个表单会会递给渲染它的动作,而这是 Html.BeginForm 在生成一个 HTML 表单时所假设的默认情况。然而 Create 方法并不是这样,因为用户希望将此表单回递给 Edit 动作,以便可以保持这个新创建的产品数据。为了对此进行修正(Create 动作方法调用了Edit 视图,当用户在此视图的表单中编辑数据然后进行递交时,默认会被回递给 Create 动作方法,但用户希望表单被回递给 Edit 动作方法,故需要修正),可以用重载的 Html.BeginForm 辅助器方法加以指明:在 Edit 视图中生成的表单的目标(始终)是 Admin 控制器的 Edit 动作方法,如下代码,它说明了对 Views/Admin/Edit.cshtml 视图文件所做的修改。
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = "Admin: Edit " + @Model.Name; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h1>Edit @Model.Name</h1> @using (Html.BeginForm("Edit", "Admin")) { @Html.EditorForModel(); <input type="submit" value="Save" /> @Html.ActionLink("Cancel and return to List", "Index"); }
经此修正之后,此表单将总是被递交给 Edit 动作,而不管渲染它的是哪个动作。
1.6 删除产品
添加对物品进行删除的支持相当简单。首先把一个新方法添加到 IProductRepository 接口,如下所示:
ASP.NET MVC4.0+EF+LINQ+bui+网站+角色权限管理系统
Spring MVC 3.2 Thymeleaf Ajax 片段