摘要:
在这篇文章中,我将继续完成SportsStore应用程序,让站点管理者可以管理产品列表。我将添加创建、修改和删除产品功能。
本篇文章将分模块的方式,逐个介绍SportsStore站点管理功能的开发过程。
数据管理部分
修改IProductRepository.cs接口代码文件。
1 using SportsStore.Domain.Entities; 2 using System.Collections.Generic; 3 4 namespace SportsStore.Domain.Abstract 5 { 6 public interface IProductRepository 7 { 8 IEnumerable<Product> Products { get; } 9 10 void SaveProduct(Product product); 11 12 Product DeleteProduct(int productID); 13 } 14 }
添加了两个接口方法SaveProduct和DeleteProduct。
修改EFProductRepository.cs代码文件,添加这两个新添加的接口的实现。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using System.Collections.Generic; 4 5 namespace SportsStore.Domain.Concrete 6 { 7 public class EFProductRepository : IProductRepository 8 { 9 private EFDbContext context = new EFDbContext(); 10 public IEnumerable<Product> Products 11 { 12 get 13 { 14 try 15 { 16 return context.Products; 17 } 18 catch (System.Exception e) 19 { 20 return null; 21 } 22 } 23 } 24 25 public void SaveProduct(Product product) 26 { 27 if (product.ProductID == 0) 28 { 29 context.Products.Add(product); 30 } 31 else 32 { 33 Product dbEntry = context.Products.Find(product.ProductID); 34 if (dbEntry != null) 35 { 36 dbEntry.Name = product.Name; 37 dbEntry.Description = product.Description; 38 dbEntry.Price = product.Price; 39 dbEntry.Category = product.Category; 40 } 41 } 42 context.SaveChanges(); 43 } 44 45 public Product DeleteProduct(int productID) 46 { 47 Product dbEntry = context.Products.Find(productID); 48 if (dbEntry != null) 49 { 50 context.Products.Remove(dbEntry); 51 context.SaveChanges(); 52 } 53 return dbEntry; 54 } 55 } 56 }
这两个方法SaveProduct和方法DeleteProduct,调用EntifyFramework,实现了保存(添加和修改)和删除产品功能。
产品管理控制器
添加控制器AdminController,此控制器区别于只用于显示产品的网站首页控制器ProductController,实现了增删改Action方法。
1 using SportsStore.Domain.Abstract; 2 using SportsStore.Domain.Entities; 3 using System.Linq; 4 using System.Web.Mvc; 5 6 namespace SportsStore.WebUI.Controllers 7 { 8 public class AdminController : Controller 9 { 10 private IProductRepository repository; 11 12 public AdminController(IProductRepository repo) 13 { 14 repository = repo; 15 } 16 17 public ViewResult Index() 18 { 19 return View(repository.Products); 20 } 21 22 public ViewResult Edit(int productId) 23 { 24 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 25 return View(product); 26 } 27 28 [HttpPost] 29 public ActionResult Edit(Product product) 30 { 31 if (ModelState.IsValid) 32 { 33 repository.SaveProduct(product); 34 TempData["message"] = string.Format("{0} has been saved", product.Name); 35 return RedirectToAction("Index"); 36 } 37 else 38 { 39 // there is something wrong with the data values 40 return View(product); 41 } 42 } 43 44 public ViewResult Create() 45 { 46 return View("Edit", new Product()); 47 } 48 49 [HttpPost] 50 public ActionResult Delete(int productId) 51 { 52 Product deletedProduct = repository.DeleteProduct(productId); 53 if (deletedProduct != null) 54 { 55 TempData["message"] = string.Format("{0} was deleted", deletedProduct.Name); 56 } 57 return RedirectToAction("Index"); 58 } 59 } 60 }
- 以Ninject构造函数注入方式生成属性IProductRepository repository。
- Index方法Action:返回产品列表
- Edit方法Action(Get方式):传入productId参数,返回带有这个产品信息的Edit视图。Edit视图的视图模型类是Product类型。
- Edit方法Action(Post方式):从视图上通过模型绑定返回产品product参数,如果绑定的模型验证结果返回的ModelState.IsValid为true,则保存修改后的product信息,使用Temp对象在页面上显示保存成功字符串,并返回产品列表视图。否则,返回原视图(Edit视图),并向视图传入正在编辑的产品对象,用户可以继续编辑该产品。
- Create方法Action:重用了Edit方法Action。返回Edit方法对应的视图,使用new Product()传入一个初始化的产品对象。
- Delete方法Action:使用了Post特性修饰,表示只能通过Post方式调用该Action,比Get方式更加安全。删除成功后,使用Temp对象在页面上显示删除成功字符串。
- 使用Temp对象保存和显示操作成功的字符串,而不能使用ViewData,因为ViewData只能在当前Action上使用。这里使用了RedirectToAction跳转到了另一个视图。
视图
在Shared文件夹中,添加_AdminLayout.cshtml视图。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <title>@ViewBag.Title</title> 9 </head> 10 <body> 11 <div class="navbar navbar-inverse" role="navigation"> 12 <a class="navbar-brand" href="#"> 13 <span class="hidden-xs">SPORTS STORE</span> 14 <div class="visible-xs">SPORTS</div> 15 <div class="visible-xs">STORE</div> 16 </a> 17 </div> 18 <div> 19 @if (TempData["message"] != null) 20 { 21 <div class="alert alert-success">@TempData["message"]</div> 22 } 23 @RenderBody() 24 </div> 25 </body> 26 </html>
_AdminLayout.cshtml的页面布局跟网站首页的布局类似。只是在Body的上部增加了显示操作成功的字符串的DIV元素。
产品管理的Index.cshtml视图
1 @model IEnumerable<SportsStore.Domain.Entities.Product> 2 3 @{ 4 ViewBag.Title = "Index"; 5 Layout = "~/Views/Shared/_AdminLayout.cshtml"; 6 } 7 8 <div class="panel panel-default"> 9 <div class="panel-heading"> 10 <h3>All Products</h3> 11 </div> 12 <div class="panel-body"> 13 <table class="table table-striped table-condensed table-bordered"> 14 <tr> 15 <th class="text-right">ID</th> 16 <th>Name</th> 17 <th class="text-right">Price</th> 18 <th class="text-center">Actions</th> 19 </tr> 20 @foreach (var item in Model) 21 { 22 <tr> 23 <td class="text-right">@item.ProductID</td> 24 <td> 25 @Html.ActionLink(item.Name, "Edit", new 26 { 27 item.ProductID 28 }) 29 </td> 30 <td class="text-right">@item.Price.ToString("c")</td> 31 <td class="text-center"> 32 @using (Html.BeginForm("Delete", "Admin")) 33 { 34 @Html.Hidden("ProductID", item.ProductID) 35 <input type="submit" class="btn btn-default btn-xs" value="Delete" /> 36 } 37 </td> 38 </tr> 39 } 40 </table> 41 </div> 42 <div class="panel-footer"> 43 @Html.ActionLink("Add a new product", "Create", null, new { @class = "btn btn-default" }) 44 </div> 45 </div>
- 该视图通过语句:Layout = "~/Views/Shared/_AdminLayout.cshtml";,指定它的布局视图是刚才创建的_AdminLayout.cshtml。
- 该视图通过表格的形式呈现了产品列表。显示产品名称的列是一个指向Edit方法Action的链接。每一行的最后一列放置一个删除产品的表单,表达内容是一个保存ProductID信息的隐藏元素和一个删除按钮。
- 页面底部显示一个创建产品的超链接,该链接通过css样式呈现成按钮样式。
最后是Edit.cshtml视图
1 @model SportsStore.Domain.Entities.Product 2 3 @{ 4 ViewBag.Title = "Admin: Edit " + @Model.Name; 5 Layout = "~/Views/Shared/_AdminLayout.cshtml"; 6 } 7 8 <h1>Edit @Model.Name</h1> 9 @using (Html.BeginForm("Edit", "Admin", FormMethod.Post)) 10 { 11 <div class="panel-body"> 12 @Html.HiddenFor(m => m.ProductID) 13 @foreach (var property in ViewData.ModelMetadata.Properties) 14 { 15 switch (property.PropertyName) 16 { 17 case "ProductID": 18 break; 19 default: 20 <div class="form-group"> 21 <label> 22 @(property.DisplayName ?? property.PropertyName) 23 </label> 24 @if (property.PropertyName == "Description") 25 { 26 @Html.TextArea(property.PropertyName, null, new { @class = "form-control", rows = 5 }) 27 } 28 else 29 { 30 @Html.TextBox(property.PropertyName, null, new { @class = "form-control" }) 31 } 32 </div> 33 break; 34 } 35 } 36 </div> 37 <div class="panel-footer"> 38 <input type="submit" value="Save" class="btn btn-primary" /> 39 @Html.ActionLink("Cancel and return to List", "Index", null, new { @class = "btn btn-default" }) 40 </div> 41 }
- 首先使用@Html.HiddenFor(m => m.ProductID)向页面发送保存有ProductID信息的隐藏元素。
- 使用ViewData.ModelMetadata.Properties,返回视图绑定类的所有属性。这里是Product类的所有属性。
- 如果该属性名是ProductID(ProductID属性),则不生成表单HTML元素。如果该属性是Description属性,则显示成一个TextArea,通过@class指定他的行数。否则,只显示成一个普通的text输入框。
- @(property.DisplayName ?? property.PropertyName)用于显示输入元素前面的Label元素。如果属性使用了Display特性指定了属性显示在页面上的字符串,则显示这个字符串。否则,只显示这个属性名。这个对于使用了多语言的系统非常有用。
运行程序,得到运行结果。
点击任意一个产品的链接,返回Edit视图。
如果点击按钮Add a new product,返回Create视图。
添加视图模型验证
修改Product.cs类。
1 using System.ComponentModel.DataAnnotations; 2 3 namespace SportsStore.Domain.Entities 4 { 5 public class Product 6 { 7 public int ProductID { get; set; } 8 [Required(ErrorMessage = "Please enter a product name")] 9 public string Name { get; set; } 10 [Required(ErrorMessage = "Please enter a description")] 11 public string Description { get; set; } 12 [Required] 13 [Range(0.01, double.MaxValue, ErrorMessage = "Please enter a positive price")] 14 public decimal Price { get; set; } 15 [Required(ErrorMessage = "Please specify a category")] 16 public string Category { get; set; } 17 } 18 }
需要引入名称空间System.ComponentModel.DataAnnotations;。该名称空间下包含了数量庞大的模型验证特性类。这里只使用了Required和Range。
继续编辑Edit.cshtml视图,将验证结果字符串显示在视图页面上。
1 @model SportsStore.Domain.Entities.Product 2 3 @{ 4 ViewBag.Title = "Admin: Edit " + @Model.Name; 5 Layout = "~/Views/Shared/_AdminLayout.cshtml"; 6 } 7 8 <h1>Edit @Model.Name</h1> 9 @using (Html.BeginForm("Edit", "Admin", FormMethod.Post)) 10 { 11 <div class="panel-body"> 12 @Html.HiddenFor(m => m.ProductID) 13 @foreach (var property in ViewData.ModelMetadata.Properties) 14 { 15 switch (property.PropertyName) 16 { 17 case "ProductID": 18 break; 19 default: 20 <div class="form-group"> 21 <label> 22 @(property.DisplayName ?? property.PropertyName) 23 </label> 24 @if (property.PropertyName == "Description") 25 { 26 @Html.TextArea(property.PropertyName, null, new { @class = "form-control", rows = 5 }) 27 } 28 else 29 { 30 @Html.TextBox(property.PropertyName, null, new { @class = "form-control" }) 31 } 32 </div> 33 @Html.ValidationMessage(property.PropertyName) 34 break; 35 } 36 } 37 </div> 38 <div class="panel-footer"> 39 <input type="submit" value="Save" class="btn btn-primary" /> 40 @Html.ActionLink("Cancel and return to List", "Index", null, new { @class = "btn btn-default" }) 41 </div> 42 }
这里通过在每个表单元素所在的DIV下面,使用语句@Html.ValidationMessage(property.PropertyName)返回表单验证结果。
还要修改_AdminLayout.cshtml,引入验证出现错误时所用的CSS样式表。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <link href="~/Content/ErrorStyles.css" rel="stylesheet" /> 9 <title>@ViewBag.Title</title> 10 </head> 11 <body> 12 <div class="navbar navbar-inverse" role="navigation"> 13 <a class="navbar-brand" href="#"> 14 <span class="hidden-xs">SPORTS STORE</span> 15 <div class="visible-xs">SPORTS</div> 16 <div class="visible-xs">STORE</div> 17 </a> 18 </div> 19 <div> 20 @if (TempData["message"] != null) 21 { 22 <div class="alert alert-success">@TempData["message"]</div> 23 } 24 @RenderBody() 25 </div> 26 </body> 27 </html>
运行程序,访问/Admin/Edit视图。如果清空表单元素,则返回验证失败的Edit视图。错误消息显示在每个表单元素的下面。
如果表单元素填写正确
点击Save按钮,则保存成功。并返回Index视图,在Index视图上看到新创建的产品。在产品列表上部,显示保存成功的字符串。
如果在Index视图上,删除Daniel这个产品。删除成功后,返回Index视图。
为表单添加客户端验证
现在使用的是服务器端验证。也就是说验证必须是发送到服务器端完成的。可以使用客户端验证方式,加快页面的访问速度。
首先使用NutGet向SportsStore.WebUI工程,添加javascript包Microsoft.jQuery.Unobtrusive.Validation 。
它将同时安装jquery.validate和jquery.validate.unobtrusive。
安装完成后的scrips文件夹内容是这样的。
修改_AdminLayout.cshtml视图,添加对新的JavaScript引用。
1 <!DOCTYPE html> 2 3 <html> 4 <head> 5 <meta name="viewport" content="width=device-width" /> 6 <link href="~/Content/bootstrap.css" rel="stylesheet" /> 7 <link href="~/Content/bootstrap-theme.css" rel="stylesheet" /> 8 <link href="~/Content/ErrorStyles.css" rel="stylesheet" /> 9 <script src="~/Scripts/jquery-1.9.1.js"></script> 10 <script src="~/Scripts/jquery.validate.js"></script> 11 <script src="~/Scripts/jquery.validate.unobtrusive.js"></script> 12 <title>@ViewBag.Title</title> 13 </head> 14 <body> 15 <div class="navbar navbar-inverse" role="navigation"> 16 <a class="navbar-brand" href="#"> 17 <span class="hidden-xs">SPORTS STORE</span> 18 <div class="visible-xs">SPORTS</div> 19 <div class="visible-xs">STORE</div> 20 </a> 21 </div> 22 <div> 23 @if (TempData["message"] != null) 24 { 25 <div class="alert alert-success">@TempData["message"]</div> 26 } 27 @RenderBody() 28 </div> 29 </body> 30 </html>
运行程序,在/Admin/Create视图上,在空的表单元素页面上点击Save按钮,发现新的表达验证直接在客户端完成了。
你也可以修改Web.config文件里的ClientValidationEnabled属性和UnobtrusiveJavaScriptEnabled属性,将他们都改为false(默认值都为true),来禁用客户端验证(变成服务器端验证)。
添加Admin路由规则
这时候,如果我们访问URL:/Admin,将继续返回Product控制器的List视图,将Admin字符串作为category参数传入List方法Action。而此时,我们希望访问/Admin/Index视图。
此时我们需要修改RouteConfig.cs代码文件的方法RegisterRoutes,向路由表头部插入新的路由规则(新的路由规则代码放在第二个位置)。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.Mvc; 6 using System.Web.Routing; 7 8 namespace SportsStore 9 { 10 public class RouteConfig 11 { 12 public static void RegisterRoutes(RouteCollection routes) 13 { 14 routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 16 routes.MapRoute( 17 name: null, 18 url: "", 19 defaults: new { controller = "Product", action = "List", category = (string)null, page = 1 } 20 ); 21 22 routes.MapRoute( 23 name: null, 24 url: "Admin", 25 defaults: new { controller = "Admin", action = "Index" } 26 ); 27 28 routes.MapRoute( 29 name: null, 30 url: "Page{page}", 31 defaults: new { controller = "Product", action = "List", category = (string)null }, 32 constraints: new { page = @"d+" } 33 ); 34 35 routes.MapRoute( 36 name: null, 37 url: "{category}", 38 defaults: new { controller = "Product", action = "List", page = 1 } 39 ); 40 41 routes.MapRoute( 42 name: null, 43 url: "{category}/Page{page}", 44 defaults: new { controller = "Product", action = "List" }, 45 constraints: new { page = @"d+" } 46 ); 47 48 routes.MapRoute( 49 name: "Default", 50 url: "{controller}/{action}/{id}", 51 defaults: new { controller = "Product", action = "List", id = UrlParameter.Optional } 52 ); 53 } 54 } 55 }
新的路由规则的url是Admin,控制器是AdminController,Action是Index方法。
这时运行程序,访问/Admin页面,将得到/Admin/Index视图。
而其他的路由规则均不受影响。