【Pro ASP.NET MVC 3 Framework】.学习笔记.8.SportsStore:管理

管理功能,如何身份认证,对controller和action方法过滤安全的访问,并在用户需要时提供证书。

1 添加分类管理

方便管理的controller,有两类页面,List页面和edit页面。

1.1 创建CRUD Controller

在Controller文件夹上点右键,创建带CRUD的controller。我们要展示如何构建controller,并解释每个步骤,删除所有的方法,只留构造函数。

1 public class AdminController : Controller 2 { 3 private IProductRepository repository; 4 5 public AdminController(IProductRepository repo) 6 { 7 repository = repo; 8 } 9 }

1.2 用Repository中的产品渲染一个Grid

添加Index方法,显示repository中的所有产品。

1 public ViewResult Index() 2 { 3 return View(repository.Products); 4 }

1.2.1 Index action的单元测试

Index方法能正确地返回repository中的所有Product对象。

1 [TestMethod] 2 public void Index_Contains_All_Products() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( 6 new Product[] { 7 new Product{ProductID = 1 ,Name = " P1 " }, 8 new Product{ProductID = 2 ,Name = " P2 " }, 9 new Product{ProductID = 3 ,Name = " P3 " } 10 }.AsQueryable()); 11 12 // Arrange - create a controller 13 AdminController target = new AdminController(mock.Object); 14 15 // Action 16 Product[] result = ((IEnumerable < Product > )target.Index().ViewData.Model).ToArray(); 17 18 // Assert 19 Assert.AreEqual(result.Length, 3 ); 20 Assert.AreEqual( " P1 " , result[ 0 ].Name); 21 Assert.AreEqual( " P2 " , result[ 1 ].Name); 22 Assert.AreEqual( " P3 " , result[ 2 ].Name); 23 }

1.3 创建一个新视图

在/vie/shared中创建_AdminLayout.cshtml,布局文件名约定以_开头。

1 < link href = " @Url.Content( " ~/ Content / Admin.css " ) " rel = " stylesheet " type = " text/css " />

引用CSS文件。

1.3 实现List View

创建AdminController的Index方法的视图,选择强类型视图,模型类是Product,使用layout文件,选在刚刚建立的_AdminLayout布局文件。并将 scaffold view(支架模型)设为List。选择了List的支架,VS会假设你使用IEnumerable序列作为模型视图的类型。

1 @model IEnumerable < SportsStore.Domain.Entities.Product > 2 3 @{ 4 ViewBag.Title = " Index " ; 5 Layout = " ~/Views/Shared/_AdminLayout.cshtml " ; 6 } 7 8 < h1 > All Products </ h1 > 9 10 < table class = " Grid " > 11 < tr > 12 < th > ID </ th > 13 < th > 14 Name 15 </ th > 16 < th class = " NumericCol " > 17 Price 18 </ th > 19 < th > 20 Actions 21 </ th > 22 </ tr > 23 24 @foreach (var item in Model) { 25 < tr > 26 < td > 27 @item.ProductID 28 </ td > 29 < td > 30 @Html.ActionLink(item.Name, " Edit " , new {item.ProductID}) 31 </ td > 32 < td class = " NumericCol " > 33 @item.Price.ToString( " c " ) 34 </ td > 35 < td > 36 @using(Html.BeginForm( " Delete " , " Admin " )){ 37 @Html.Hidden( " ProductID " ,item.ProductID) 38 < input type = " submit " value = " Delete " /> 39 } 40 </ td > 41 42 </ tr > 43 } 44 45 </ table > 46 < p > @Html.ActionLink( " Add a new product " , " Create " ) </ p >

1.4 编辑Products

要提供创建和更新特性,我们将添加产品编辑页面。

  • 显示一个页面,允许管理员改变产品属性的值
  • 添加一个action方法,提交改变后处理

1.4.1 创建Edit Action方法

1 [TestMethod] 2 public void Can_Edit_Product() 3 { 4 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 5 mock.Setup(m => m.Products).Returns( 6 new Product[] { 7 new Product{ProductID = 1 ,Name = " P1 " }, 8 new Product{ProductID = 2 ,Name = " P2 " }, 9 new Product{ProductID = 3 ,Name = " P3 " } 10 }.AsQueryable()); 11 12 // Arrange - create a controller 13 AdminController target = new AdminController(mock.Object); 14 15 // Action 16 Product p1 = target.Edit( 1 ).ViewData.Model as Product; 17 Product p2 = target.Edit( 2 ).ViewData.Model as Product; 18 Product p3 = target.Edit( 3 ).ViewData.Model as Product; 19 20 // Assert 21 Assert.AreEqual( 1 , p1.ProductID); 22 Assert.AreEqual( 2 , p2.ProductID); 23 Assert.AreEqual( 3 , p3.ProductID); 24 } 25 26 [TestMethod] 27 public void Cannot_Edit_Nonexistent_Product() 28 { 29 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 30 mock.Setup(m => m.Products).Returns( 31 new Product[] { 32 new Product{ProductID = 1 ,Name = " P1 " }, 33 new Product{ProductID = 2 ,Name = " P2 " }, 34 new Product{ProductID = 3 ,Name = " P3 " } 35 }.AsQueryable()); 36 37 // Arrange - create a controller 38 AdminController target = new AdminController(mock.Object); 39 40 // Action 41 Product result = target.Edit( 4 ).ViewData.Model as Product; 42 43 // Assert 44 Assert.IsNull(result); 45 }

1.4.2 创建Edit视图

使用强类型视图,模型类为Product。可以使用支架中的edit,但是我们使用Empty。使用_AdminLayout的布局文件。

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()){ 10 @Html.EditorForModel() 11 < input type = " submit " value = " Save " /> 12 @Html.ActionLink( " Cancel and return to List " , " Index " ) 13 }

与手工地写每个label和inputs相比,我们调用Html.EditorForModel helper方法。这个方法请求MVC框架,创建编辑界面,它会检查模型的类型。EditorForModel很方便,但不能产生最吸引人的结果。我们不想让管理员看到或编辑ProductID属性,并且描述属性的文本框太小。

我们可以使用model metadate(模型元数据),给MVC框架致命怎样为属性创建编辑器。这允许我们,对属性使用特性,来影响Html.EditorForModel方法的输出。

更新Product类

1 public class Product 2 { 3 [HiddenInput(DisplayValue = false )] 4 public int ProductID { get ; set ; } 5 6 public string Name { get ; set ; } 7 8 [DataType(DataType.MultilineText)] 9 public string Description { get ; set ; } 10 11 public decimal Price { get ; set ; } 12 public string Category { get ; set ; } 13 }

HiddenInput需要添加System.Web.Mvc的引用。DateType需要添加System.ComponentModel.DataAnnotations的引用。HiddenInput属性告诉MVC框架,将这个属性渲染为隐藏的表元素。DataType属性允许我们指定值时如何呈现和编辑。

界面依然很简陋,我们可以使用CSS改善。当MVC框架为每个属性创建input fields,它指派不同的CSS classes。textarea元素上有class=”text-box multi-line”。我们改变它,在Content文件夹下更改Admin.css。页面模板视图助手EditorForModel不是总符合我们的需求,我们将会自定义。

1.4.3 更新Product Repository

要处理编辑前,我们得增强product repository,才能保存更改。给IProductRepository接口新增方法。

1 public interface IProductRepository 2 { 3 IQueryable < Product > Products { get ; } 4 void SaveProduct(Product product); 5 }

EF实现的repository,即EFProductRepository类中添加这个方法

1 public void SaveProduct(Product product) 2 { 3 if (product.ProductID == 0 ){ 4 context.Products.Add(product); 5 } 6 context.SaveChanges(); 7 }

SaveChanges方法的实现,如果ProductID是0就添加一个Product给repository。它接受任何对现存Product的更改。

1.4.4 处理Edit POST 请求

当管理员点击Save按钮,Edit action方法会处理POST请求。

1 [HttpPost] 2 public ActionResult Edit(Product product) 3 { 4 if (ModelState.IsValid) 5 { 6 repository.SaveProduct(product); 7 TempData[ " message " ] = string .Format( " {0} has been saved " , product.Name); 8 return RedirectToAction( " Index " ); 9 } 10 else 11 { 12 return View(product); 13 } 14 }

先检查模型绑定已经验证用户提交的输数据。如果一切OK,保存变更到repositoy,然后调用Index action方法,返回到产品列表页面。如果有问题,再次渲染Edit视图,让用户更正。

在我们保存变更到repository后,我们使用TempData特性存储一个消息。这是键值类型的字典,和session data和View Bag相似。关键的不同之处是TempData会在HTTP request最后被删除。

注意我们从Edit方法返回了ActionResult类型。之前偶们都是用ViewResult类型,ViewResult是派生自ActionResult,当你想让框架渲染一个视图的时候可以使用。然而,其他类型可以使用ActionResult,RedirectToAction就是其中的一个。我们在Edit action方法中,用它调用Index action方法。

在这种情况下,用户被重定向,我们可以使用VeiwBag。ViewBag在controller和view之间传递数据,它不能比当前HTTP请求更长时间地持有数据。偶们可以使用session data特性,但是消息会在偶们明确地移除它时才删除,我们不想这样做。所以,TempData特使非常适合。数据被限制为单一用户的session(所以用户看不到其他用户的TempData),并且存留到我们阅读它。我们会在视图被action方法渲染后阅读数据。

1.4.5 Edit提交的单元测试

我们要确保对Product有效的更新,模型绑定已经被创建,传递给product repository保存。我们也想检查无效的更新,不会传给repository。

1 [TestMethod] 2 public void Can_Save_Valid_Changes() 3 { 4 // Arrange - create mock repository 5 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 6 // Arrange - create the controller 7 AdminController target = new AdminController(mock.Object); 8 // Arrange - create the product 9 Product product = new Product { Name = " Test " }; 10 11 // Act - try to save the Product 12 ActionResult result = target.Edit(product); 13 14 // Assert - check that the repository was called 15 mock.Verify(m => m.SaveProduct(product)); 16 // Assert - check the method result type 17 Assert.IsNotInstanceOfType(result, typeof (ViewResult)); 18 } 19 20 [TestMethod] 21 public void Cannot_Save_Invalid_Changes() 22 { 23 // Arrange - create mock repository 24 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 25 // Arrange - create the controller 26 AdminController target = new AdminController(mock.Object); 27 // Arrange - create a product 28 Product product = new Product { Name = " Test " }; 29 // Arrange - add an error to the model state 30 target.ModelState.AddModelError( " error " , " error " ); 31 32 // Act - try to save the product 33 ActionResult result = target.Edit(product); 34 35 // Assert - check that the repository was not called 36 mock.Verify(m => m.SaveProduct(It.IsAny < Product > ()), Times.Never()); 37 // Assert - check the method result type 38 Assert.IsInstanceOfType(result, typeof (ViewResult)); 39 }

1.4.6 显示确认消息

在_AdminLaout.cshtml布局上显示TempData的消息。通过处理模板上的消息,我们可以在任何使用模板的视图上创建消息,而不用创建附加的Razor块。

1 < div > 2 @if(TempData[ " message " ] != null ){ 3 < div class = " Message " > @TempData[ " message " ] </ div > 4 } 5 @RenderBody() 6 </ div >

这样做的好处是,无论用户打开哪个页面,只要使用相同的layout,即使改变了工作流的其他页面,用户也会看到消息。如果你重新载入页面,消息会小时,因为TempData会在阅读后被删除。

1.4.7 添加模型校验

像为ShippingDetails类一样,为Product类添加模型校验

1 public class Product 2 { 3 [HiddenInput(DisplayValue = false )] 4 public int ProductID { get ; set ; } 5 6 [Required(ErrorMessage = " Please enter a product name " )] 7 public string Name { get ; set ; } 8 9 [Required(ErrorMessage = " Please enter a description " )] 10 [DataType(DataType.MultilineText)] 11 public string Description { get ; set ; } 12 13 [Required] 14 [Range( 0.01 , double .MaxValue,ErrorMessage = " Please enter a positive price " )] 15 public decimal Price { get ; set ; } 16 17 [Required(ErrorMessage = " Please specify a category " )] 18 public string Category { get ; set ; } 19 }

可以将这些限制属性移动到其他类中,并告诉MVC如何找到他们。

当使用Html.EditorForModel helper方法创建表单元素时,MVC框架会给inline加进markup和CSS。

1.4.8 启用客户端校验

MVC框架可以基于我们领域模型类中使用的data annotations执行客户端校验。这个特性默认启用,但是它还没有工作,因为哦我们没有添加必须的JavaScript库的链接。在_AdminLayout.cshtml文件上链接JavaScript库,可以在任何使用这个布局的页面上客户端校验。

1 < script src ="@Url.Content(" ~/Scripts/jquery-1.4.4.min.js")" type ="text/javascript" ></ script > 2 < script src ="@Url.Content(" ~/Scripts/jquery.validate.min.js")" type ="text/javascript" ></ script > 3 < script src ="@Url.Content(" ~/Scripts/jquery.validate.unobtrusive.min.js")" type ="text/javascript" ></ script >

使用客户端校验,会立即响应,并且不需要将请求发送到服务器。

如果你不想启用当前action的客户端校验,需要在view或controller中使用下面的声明

1 HtmlHelper.ClientValidationEnabled = false; 2 HtmlHelper.UnobtrusiveJavaScriptEnabled = false;

要禁用整个程序的客户端校验,需要将上面的声明添加到Global.asax的Application_Start方法中。或在Web.config文件中加入下面:

1 < configuration > 2 < appSettings > 3 < add key ="ClientValidationEnabled" value ="false" /> 4 < add key ="UnobtrusiveJavaScriptEnabled" value ="false" /> 5 </ appSettings > 6 </ configuration >

1.5 创建新产品

在AdminController中创建新方法

1 public ViewResult Create() 2 { 3 return View("Edit", new Product()); 4 }

Create方法没有渲染它的默认视图,而是指定了Edit视图。这是完美的可接受的,一个action方法使用总是关联其他view的view。在这个例子中,我们注入一个新的Product对象,做诶视图模型,Edit视图使用空字段填充。

1 < form action ="/Admin/Create" method ="post" >

此时Html.BeginForm默认产生的表单,action为条用它的action,即Create。只有action为Edit时,才能正常编辑。要修复这点,我们可以使用html.BeginForm helper方法的重载版本,来指定触发表单生成的action和congtroller是Edit和Admin。

1 Html.BeginForm("Edit","Admin") 2 < form action ="/Admin/Edit" method ="post" >

1.6 删除Products

要添加delete是非常简单,首先要在IProductRepository接口添加新方法。

1 public interface IProductRepository 2 { 3 IQueryable < Product > Products { get ; } 4 void SaveProduct(Product product); 5 void DeleteProduct(Product product); 6 } 7 public void DeleteProduct(Product product) 8 { 9 context.Products.Remove(product); 10 context.SaveChanges(); 11 }

最后需要在AdminController中实现Delete action方法。这个方法必须仅支持POST请求,因为产出对象不是一个等幂操作。浏览器和缓存可以造出GET请求,而不用用户明确地同意。所以我们必须小心避免改变Get请求的结果。

1 [HttpPost] 2 public ActionResult Delete( int productId) 3 { 4 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 5 if (product != null ){ 6 repository.DeleteProduct(product); 7 TempData[ " message " ] = string .Format( " {0} was deleted " , product.Name); 8 } 9 return RedirectToAction( " Index " ); 10 }

1.6.1 删除产品的单元测试

我们想要测试两个特性,第一个是当一个有效的ProductID作为参数传递给action方法,它调用repository的DeleteProduct方法,并传递正确的、要删除的Product对象。

第二个测试是确保如果传递给Delete方法的参数值,不是repositrory中的可用Product,repository的DeleteProduct方法没有被调用。

1 [TestMethod] 2 public void Can_Delete_Valid_Products() 3 { 4 // Arrange - create a Product 5 Product prod = new Product { ProductID = 2 , Name = " Test " }; 6 7 // Arrange - create the mock repository 8 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 9 mock.Setup(m => m.Products).Returns( new Product[] { 10 new Product {ProductID = 1 , Name = " P1 " }, 11 prod, 12 new Product {ProductID = 3 , Name = " P3 " }, 13 }.AsQueryable()); 14 15 // Arrange - create the controller 16 AdminController target = new AdminController(mock.Object); 17 18 // Act - delete the product 19 target.Delete(prod.ProductID); 20 21 // Assert - ensure that the repository delete method was 22 // called with the correct Product 23 mock.Verify(m => m.DeleteProduct(prod)); 24 } 25 26 [TestMethod] 27 public void Cannot_Delete_Invalid_Products() 28 { 29 30 // Arrange - create the mock repository 31 Mock < IProductRepository > mock = new Mock < IProductRepository > (); 32 mock.Setup(m => m.Products).Returns( new Product[] { 33 new Product {ProductID = 1 , Name = " P1 " }, 34 new Product {ProductID = 2 , Name = " P2 " }, 35 new Product {ProductID = 3 , Name = " P3 " }, 36 }.AsQueryable()); 37 38 // Arrange - create the controller 39 AdminController target = new AdminController(mock.Object); 40 // Act - delete using an ID that doesn't exist 41 target.Delete( 100 ); 42 43 // Assert - ensure that the repository delete method was 44 // called with the correct Product 45 mock.Verify(m => m.DeleteProduct(It.IsAny < Product > ()), Times.Never()); 46 }

你可能感兴趣的:(framework)