本文继续构建SportsStore应用程序,为网站管理员提供一个管理产品的方法。
管理条目集合的惯例,是向用户显示两种形式的页面,一个列表页和一个编辑页。页面合起来可以让用户创建、读取、更新和删除集合中的条目。这些动作统称为“CRUD”,开发人员往往需要实现CRUD,因此VS通常会设法对此提供帮助,以便生成具有CRUD操作动作方法的MVC控制器,同时也提供对这些操作进行支持的视图。
一:创建CRUD控制器
本小节将创建一个新的控制器来处理这些管理功能。右击SportsStore.WebUI项目的Controllers文件夹,从弹出菜单中选择添加控制器,将该控制器命名为AdminController,模板选择为空的MVC控制器。添加一个动作方法,用来显示存储库中的所有产品,将其方法命名为Index,如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstrace; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Controllers { [Authorize] public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } // GET: Admin public ActionResult Index() { return View(repository.Products); } }
二:创建新的布局
本例打算创建一个新的Razor布局,以用于SportsStore的管理视图。右击SportsStore.WebUI项目的Views/Shared文件夹,选择添加-MVC5布局页,将其命名为_AdminLayout.cshtml,如下所示。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - 后台管理</title> @Styles.Render("~/Content/css") @Styles.Render("~/Content/admincss") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div> <div> <button type="button" data-toggle="collapse" data-target=".navbar-collapse"> <span></span> <span></span> <span></span> </button> @Html.ActionLink("运动商城", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("首页", "Index", "Home")</li> </ul> </div> </div> </div> <div class="container body-content"> @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - 运动商城</p> </footer> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html>
三:实现List视图
现在已经创建了一个新的布局页,下面可以对项目添加一个视图,用于Admin控制器的Index动作方法。在Index方法中右击,选择添加视图,在添加视图对话框中,视图名称命名为Index,模板选择List,模型类选择Product,选中使用布局页复选框,布局选择刚刚新建的_AdminLayout.cshtml文件,如下所示。
点击添加,稍等片刻VS将创建视图如下所示。
@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>
VS会考察视图模型对象的类型,并根据该模型所定义的属性,生成一些表格形式的元素。启动应用程序,导航到Admin/Index地址,我们将会看到如下渲染结果。
List模板为用户做了很好的设置工作。此时得到了Product类中各个属性的表格列,以及进行CRUD操作的链接,这些链接指向同一个控制器的各个动作方法。但是这些标记有些冗长,而且样式也不美观,我们需要重新编辑此文件,如下所示。
@model IEnumerable<SportsStore.Domain.Entities.Product> @{ ViewBag.Title = "产品管理"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>产品管理</h2> <hr /> <div> <table class="table table-hover"> <thead> <tr> <th>ID</th> <th>名称</th> <th>价格</th> <th>操作</th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td>@item.ProductID</td> <td>@Html.ActionLink(item.Name,"Edit",new { item.ProductID})</td> <td>@item.Price.ToString("c")</td> <td> @using (Html.BeginForm("Delete","Admin")) { @Html.Hidden("ProductID",item.ProductID) <input type="submit" value="删除" class="btn btn-danger btn-sm" /> } </td> </tr> } </tbody> </table> </div> <p> @Html.ActionLink("新建产品", "Create") </p>
修改后的视图以一种更紧凑的形式表现了相关信息,它忽略了Product类的一些属性,并用一种不同的办法展示了指向产品的链接。启动应用程序,我们将会看到如下渲染。
四:编辑产品
为了提供创建和更新特性,将添加一个产品编辑页面。此工作有两个部分:显示一个让管理员能够修改产品属性的页面;添加一个动作方法,它能够在提交时对这些修改进行处理。
4.1创建Edit动作方法
在AdminController类中添加Edit方法,如下所示。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using SportsStore.Domain.Abstrace; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Controllers { public class AdminController : Controller { private IProductRepository repository; public AdminController(IProductRepository repo) { repository = repo; } // GET: Admin public ActionResult Index() { return View(repository.Products); } public ActionResult Edit(int productId) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); return View(product); } } }
这个简单的方法找出与productId参数对应ID的产品,并把它作为一个视图模型对象进行传递。
4.2创建Edit视图
右击Edit方法,选择添加视图,视图命名为Edit,模型类选择Product。也可以使用Edit模板来创建视图,本例不再演示。新建的Edit视图如下所示。
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = Model.ProductID > 0 ? "产品编辑" : "产品添加"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>@(Model.ProductID > 0 ? "产品编辑" : "产品添加")</h2> <hr /> <div> @using (Html.BeginForm()) { @Html.EditorForModel() <input type="submit" value="保存" class="btn btn-primary" /> @Html.ActionLink("取消并返回列表", "Index") } </div>
此例并未手工为每个标签和输入项编写标记,而是调用了Html.EditorForModel辅助器方法。这个方法要求MVC框架创建编辑界面,这是通过探测其模型类型来实现的。
运行应用程序并导航到/Admin/Index,点击一个产品名,将会看到如下所示的页面。
EditorForModel方法很方便,但它产生的结果并不令人十分满意。比如:我们并不希望管理员可以看到或编辑ProductID属性;Description的输入框也太小了;输入框名也是直接显示的模型类属性名。
解决以上问题,我们可以运用注解属性,将注解属性运用于模型类的属性上,以影响Html.EditorForModel方法的输出,编辑Product类文件如下所示。
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SportsStore.Domain.Entities { [Serializable()] public class Product { [HiddenInput(DisplayValue =false)] public int ProductID { get; set; } [DisplayName("名称")] public string Name { get; set; } [DataType(DataType.MultilineText)] [DisplayName("简介")] public string Description { get; set; } [DisplayName("价格")] public decimal Price { get; set; } [DisplayName("类别")] public string Category { get; set; } } }
HiddenInput注解属性告诉MVC框架,将该属性渲染为隐藏的表单元素,DisplayName注解属性设置编辑框的名称,而DataType注解属性能够指示如何显示或编辑一个值。本例选择了MultilineText选项。此时再次运行应用程序,打开一个产品的编辑页面,将会看到如下渲染。
4.3更新产品存储库
在能够处理编辑之前,需要增强产品存储库,才能保证所作的修改。首先要对IProductRepository接口添加一个新的方法,如下所示。
using System.Linq; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstrace { public interface IProductRepository { IQueryable<Product> Products { get; } void SaveProduct(Product product); } } 然后将这个方法添加到存储库的EF实现上,编辑EFProductRepository类,如下所示。 using SportsStore.Domain.Abstrace; 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; dbEntry.ImageData = product.ImageData; dbEntry.ImageMimeType = product.ImageMimeType; } } context.SaveChanges(); } } }
如果ProductID为0,这个SaveChanges方法实现会将一个产品加入存储库;否则会将任何修改运用于数据库中已存在的物品。
4.4处理Edit的POST请求
此刻已经做好了准备,以便在Admin控制器中实现一个重载的Edit方法,它将在管理员点击保存按钮时处理POST请求,新方法如下所示。
using SportsStore.Domain.Abstrace; using SportsStore.Domain.Entities; 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; } // GET: Admin public ActionResult Index() { return View(repository.Products); } public ActionResult 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["msg"] = string.Format("{0}已成功保存", product.Name); return RedirectToAction("Index"); } else return View(product); } } }
在存储库保存了修改后,用TempData特性保存了一条消息,它类似于之前已经使用过的会话数据和ViewBag特性。与会话数据的关键差异是,TempData在HTTP请求结束时会被删除。
4.5显示确认消息
本例打算在_AdminLayout.cshtml布局文件中处理TempData存储的消息。通过在布局文件中处理消息,可以在任何使用此布局的视图中创建消息,而不需要创建额外的Rozar块。编辑_AdminLayout.cshtml如下所示。
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - 后台管理</title> @Styles.Render("~/Content/css") @Styles.Render("~/Content/admincss") @Scripts.Render("~/bundles/modernizr") </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div> <div> <button type="button" data-toggle="collapse" data-target=".navbar-collapse"> <span></span> <span></span> <span></span> </button> @Html.ActionLink("运动商城", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("首页", "Index", "Home")</li> </ul> </div> </div> </div> <div class="container body-content"> @if (TempData["msg"] != null) { <div class="alert alert-info alert-dismissable" style="margin-top:20px;"> <button type="button" data-dismiss="alert" aria-hidden="true"> × </button> @TempData["msg"]</div> } @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - 运动商城</p> </footer> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @RenderSection("scripts", required: false) </body> </html>
现在有了对产品编辑进行测试的所有元素,运行应用程序,导航到Admin/Index,编辑一个产品,点击保存按钮,系统将返回到列表视图,并显示TempData消息,如下所示。
如果刷新页面,这条消息将会消失,这是因为TempData在被读取时就被删除了。
4.6添加模型验证
通常情况下,需要对模型实体添加验证规则,编辑Product类文件,如下所示。
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SportsStore.Domain.Entities { [Serializable()] public class Product { [HiddenInput(DisplayValue =false)] public int ProductID { get; set; } [DisplayName("名称")] [Required(ErrorMessage ="请输入{0}")] public string Name { get; set; } [DataType(DataType.MultilineText)] [DisplayName("简介")] [Required(ErrorMessage = "请输入{0}")] public string Description { get; set; } [DisplayName("价格")] [Required(ErrorMessage = "请输入{0}")] [Range(0.01, double.MaxValue, ErrorMessage = "{0}必须在{1}和{2}之间")] public decimal Price { get; set; } [DisplayName("类别")] [Required(ErrorMessage = "请输入{0}")] public string Category { get; set; } } }
使用Html.EditorForModel辅助器方法来创建Form元素以编辑Product时,MVC框架添加了显示内联的验证错误所需的所有标记和CSS。当编辑一个产品而不符合验证规则时,会出现如下所示的界面。
4.7启用客户端验证
此时,只有当管理员把编辑提交给服务器时,才会运用数据验证。大多数Web用户希望,如果输入的数据有问题,要立即得到反馈。数据在浏览器中用Javascript进行检查,MVC框架可以根据运用于域模型类的数据注解来执行客户端验证。
这一特性是默认可用的,但它尚不会生效,因为还没有添加对所需的JavaScript库的链接。编辑_AdminLayout.cshtml如下所示。
<div class="container body-content"> @if (TempData["msg"] != null) { <div class="alert alert-info alert-dismissable" style="margin-top:20px;"> <button type="button" data-dismiss="alert" aria-hidden="true"> × </button> @TempData["msg"]</div> } @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - 运动商城</p> </footer> </div> @Scripts.Render("~/bundles/jquery", "~/bundles/jqueryval", "~/bundles/bootstrap") @RenderSection("scripts", required: false)
通过Scripts.Render添加jquery.validate库,客户端验证将对管理视图生效。显示给用户错误消息的外观是相同的,因为服务器端验证所使用的CSS的class也由客户端验证所使用。客户端验证得到的相应是及时的,并且不需要把请求发送到服务器。某些情况下不希望在客户端验证时,可以使用以下语句。
HtmlHelper.ClientValidationEnabled = false;
HtmlHelper.UnobtrusiveJavaScriptEnabled = false;
如果把上面的语句放在一个视图或一个控制器中,那么客户端验证只对当前的动作失效。要禁用整个应用程序的客户端验证,可以在Global.asax的Application_Start方法中使用上面的语句,或是把这些值运用于Web.config文件,代码如下。
<configuration> <appSettings> <add key="ClientValidationEnabled" value="false" /> <add key="UnobtrusiveJavaScriptEnabled" value="false" /> </appSettings> <connectionStrings>
4.8 Scripts.Render和Styles.Render
在_AdminLayout.cshtml以及之前的默认布局页中,我们使用了Scripts.Render和Styles.Render特性来引用js库和CSS文件。
Scripts.Render是asp.net mvc用于优化页面请求的技术。基本功能与在页面中直接书写<script>标签是一样的。但是通过@script.Render方法,你可以预定义一组js文件,在最终页面上,asp.net mvc自身会采用比较优化的压缩技术和缓存技术,将多个js压缩优化并且整合为1个体积较小的js,但对于外部使用来说,功能是不会出现任何区别的,从而提高了页面体验。而压缩和缓存的过程都是通过框架自动实现的,你只需要按照缩进和自己的习惯写优雅的js代码即可。
使用Scripts.Render或Styles.Render特性,首先要在App_Start 里面BundleConfig.cs 文件里面添加要包含的js或css文件,BundleConfig就是一个微软新加的一个打包的配置类,BundleConfig用来Add 各种Bundle,BundleConfig配置信息如下:
using System.Web; using System.Web.Optimization; namespace SportsStore.WebUI { public class BundleConfig { // 有关捆绑的详细信息,请访问 https://go.microsoft.com/fwlink/?LinkId=301862 public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.validate*")); // 使用要用于开发和学习的 Modernizr 的开发版本。然后,当你做好 // 生产准备就绪,请使用 https://modernizr.com 上的生成工具仅选择所需的测试。 bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( "~/Scripts/bootstrap.js", "~/Scripts/respond.js")); bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/bootstrap.css", "~/Content/site.css")); bundles.Add(new StyleBundle("~/Content/admincss").Include( "~/Content/Admin.css")); } } }
然后在视图文件中使用Scripts.Render()输出脚本包,Styles.Render()输出样式包:
Script文件引用:@Scripts.Render(virtualPath[,virtualPath1][,virtualPath2][,...])
CSS文件引用:@Styles.Render(virtualPath[,virtualPath1][,virtualPath2][,...])
如下所示:
@Scripts.Render("~/bundles/jquery", "~/bundles/jqueryval", "~/bundles/bootstrap") @Styles.Render("~/Content/admincss")
五:创建新产品
下一步将实现Create动作方法,这是在产品列表页中“新建产品”链接所指定的方法。对AdminController类添加Create方法,如下所示。
public ActionResult Create() { return View("Edit", new Product()); }
这个Create方法并不渲染它的默认视图,而是指明应该使用Edit视图。我们还需要修改Edit视图中的Html.BeginForm辅助器方法,以使生成表单的目标始终是Admin控制器的Edit方法,如下所示。
@model SportsStore.Domain.Entities.Product @{ ViewBag.Title = Model.ProductID > 0 ? "产品编辑" : "产品添加"; Layout = "~/Views/Shared/_AdminLayout.cshtml"; } <h2>@(Model.ProductID > 0 ? "产品编辑" : "产品添加")</h2> <hr /> <div> @using (Html.BeginForm("Edit","Admin")) { @Html.EditorForModel() <input type="submit" value="保存" class="btn btn-primary" /> @Html.ActionLink("取消并返回列表", "Index") } </div>
经过修正之后,此表单将总是被提交给Edit动作,而不管渲染它的是哪个动作。
六:删除产品
添加对物品进行删除的支持相当简单,首先把一个方法添加到IProductRepository接口,如下所示。
using System.Linq; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstrace { public interface IProductRepository { IQueryable<Product> Products { get; } void SaveProduct(Product product); Product DeleteProduct(int productID); } }
下一步,在EFProductRepository中实现这个方法,如下所示。
using SportsStore.Domain.Abstrace; 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; dbEntry.ImageData = product.ImageData; dbEntry.ImageMimeType = product.ImageMimeType; } } context.SaveChanges(); } public Product DeleteProduct(int productID) { Product dbEntry = context.Products.Find(productID); if (dbEntry!=null) { context.Products.Remove(dbEntry); context.SaveChanges(); } return dbEntry; } } }
最后一步是在Admin控制器中实现一个Delete动作方法,这个动作方法应当只支持POST请求,如下所示。
[HttpPost] public ActionResult Delete(int productID) { Product deleteProduct = repository.DeleteProduct(productID); if (deleteProduct != null) TempData["msg"] = string.Format("成功删除{0}", deleteProduct.Name); return RedirectToAction("Index"); }
现在运行应用程序,点击一个产品的删除按钮,就可以看到新功能已经起作用了。
六:小结
本文介绍了管理能力,并展示了如何实现CRUD操作,让管理员能够创建、读取、更新和删除存储库中的产品。