上一章点击https://blog.csdn.net/zpczmc/article/details/86765140查看
下一章点击https://blog.csdn.net/zpczmc/article/details/86799144查看
本章,继续构建应用程序,给产品和订单添加管理功能
管理订单
在前面的章节中,已经实现了接受客户的订单并存储在数据库中,本章中,创建一个管理功能以便实现对于接受到订单的查阅,并可以标记发货的订单
强化模型
首先我们要强化订单模型使得订单Order类可以记录自身是否被发货,这需要在Order类中增加一个属性标志订单是否发货,就是你在Listing 11-1 中看到的这样
Listing 11-1. Adding a Property in the Order.cs File in the Models Folder
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace SportsStore.Models
{
public class Order
{
[BindNever]
public int OrderID { get; set; }
[BindNever]
public ICollection<CartLine> Lines { get; set; }
//---------------------------------------------------------------------
[BindNever]
public bool Shipped { get; set; }
//---------------------------------------------------------------------
[Required(ErrorMessage = "Please enter a name")]
public string Name { get; set; }
[Required(ErrorMessage ="Please enter the first address line")]
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Line3 { get; set; }
[Required(ErrorMessage = "Please enter a city name")]
public string City { get; set; }
[Required(ErrorMessage = "Please enter a state name")]
public string State { get; set; }
public string Zip { get; set; }
[Required(ErrorMessage = "Please enter a country name")]
public string Country { get; set; }
public bool GiftWrap { get; set; }
}
}
这种针对模型的调整和拓展(属性)以实现不同的要求对于典型的MVC开发是非常普遍的,在理想的情况下,你可以在开始设计应用程序时就完全定义好模型,然后围绕它构建应用程序,但这只是适用于简单的项目,而且在实践中,随着对项目的理解的变化迭代开发是很正常的,由于存在实体框架的迁移特性,这使得您不需要手动编写SQL命令,就能实现数据库与模型类的匹配,要把我们新增加的Shipped属性反映到数据库中,打开新的命令提示符窗口,定位到StartUp.cs的文件夹路径,执行以下命令即可:
dotnet ef migrations add ShippedOrders
也可以通过PowerShell命令窗口(可通过点击[工具]→[NuGet包管理器]→[程序包管理器控制台]打开)输入以下命令
add-migration ShippedOrders
当应用程序启动并调用SeedData方法时会自动应用实体框架提供的迁移方法.
增加控制器Actions和视图
在我前几章构建的功能和基础架构的基础上实现显示和更新数据库中的订单相对简单,给控制器增加两个Action方法,如Listing 11-2 所示
Listing 11-2. Adding Action Methods in the OrderController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class OrderController : Controller
{
private IOrderRepository repository;
private Cart cart;
public OrderController(IOrderRepository repoService,Cart cartService)
{
repository = repoService;
cart = cartService;
}
//--------------------------------------------------------------------------------
public ViewResult List() => View(repository.Orders.Where(o => !o.Shipped));
[HttpPost]
public IActionResult MarkShipped(int orderID)
{
Order order = repository.Orders.FirstOrDefault(o => o.OrderID == orderID);
if (order != null)
{
order.Shipped = true;
repository.SaveOrder(order);
}
return RedirectToAction(nameof(List));
}
//--------------------------------------------------------------------------------
public ViewResult Checkout() => View(new Order());
[HttpPost]
public IActionResult Checkout(Order order)
{
if (cart.Lines.Count() == 0)
{
ModelState.AddModelError("", "Sorry,your cart is empty!");
}
if (ModelState.IsValid)
{
order.Lines = cart.Lines.ToArray();
repository.SaveOrder(order);
return RedirectToAction(nameof(Completed));
}
else
{
return View(order);
}
}
public ViewResult Completed()
{
cart.Clear();
return View();
}
}
}
List方法把所有Order对象中Shipped为false的对象传递给默认视图,这个视图用于呈现给管理员那些未发货的订单.
MarkShipped方法接受一个通过Post请求的未发货的Order对象的标识,这个标识可以定位存储库中相关的Order对象,并把它的发货属性Shipped改成true保存在数据库中,
在Views/Order文件夹下增加一个叫做List.cshtml的Razor视图用于显示未发货的订单,如Listing 11-3所示,用一个Table 元素显示哪些产品已经付款.
Listing 11-3. The Contents of the List.cshtml File in the Views/Order Folder
@model IEnumerable<Order>
@{
ViewBag.Title = "Orders";
Layout = "_AdminLayout";
}
@if (Model.Count() > 0)
{
<table class="table table-bordered table-striped">
<tr>
<th>Name</th><th>Zip</th><th colspan="2">Details</th><th></th>
</tr>
@foreach (Order o in Model)
{
<tr>
<td>@o.Name</td><td>@o.Zip</td><th>Product</th><th>Quantity</th>
<td>
<form asp-action="MarkShipped" method="post">
<input type="hidden" name="orderID" value="@o.OrderID"/>
<button type="submit" class="btn btn-sm btn-danger">Ship</button>
</form>
</td>
</tr>
@foreach (CartLine line in o.Lines)
{
<tr>
<td colspan="2"></td>
<td>@line.Product.Name</td><td>@line.Quantity</td>
<td></td>
</tr>
}
}
</table>
}
else
{
<div class="text-center">No Unshipped Orders</div>
}
每个订单会显示一个 Ship 的按钮用于给MarkShipped方法提交一个表单,用布局属性给List视图制定一个不同的布局,它将会覆盖_ViewStart.cshtml指定的默认布局(母版页),
用MVC 布局页模板在Views/Shared文件夹中创建一个叫做 _AdminLayout.cshtml的布局页,如Listing 11-4 所示
Listing 11-4. The Contents of the _AdminLayout.cshtml File in the Views/Shared Folder
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
<title>@ViewBag.Title</title>
</head>
<body class="m-1 p-1">
<div class=" bg-info p-2">
<h4>@ViewBag.Title</h4>
</div>
@RenderBody()
</body>
</html>
启动应用程序,选择一些商品,然后提交订单,导航到 Order/List URL 然后你会看到订单的大体内容,像Figure 11-1 中所示的那样,点击Ship 按钮, 数据库就会更新,然后这个列表将为空(因为没有未发货的).
注意,目前没有管理什么用户可以查看这个订单,管理订单,我将在第12章来讲这个问题
Figure 11-1. Managing orders
添加目录管理
管理复杂项目集合的一般做法是增加两种类型的网页,一个列表页和一个编辑页,就像Figure 11- 2 中看到的这样
Figure 11-2. Sketch of a CRUD UI for the product catalog
这种页面可以同时满足用户对集合元素的增删改查,这些操作统称为CURD,开发人员经常要实现CURD,VS中脚手架默认支持预定义的CURD方案(我已经在第八章说明了),但是就像所有的VS模板一样,我们尽可能不用它,因为现在我们正在学习它,我们应该直接利用ASP.NET CORE MVC的功能实现它,(直接采用模板你将不会对CURD有更深的学习).
创建一个CURD控制器
首先创建一个单独的控制器用来管理产品列表(目录),在Controllers 目录新增一个AdminController.cs的控制器类,如Listing 11-5所示
Listing 11-5. The Contents of the AdminController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
namespace SportsStore.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Products);
}
}
这个控制器构造函数声明了对IProductRepository接口的依赖性,在创建AdminController类时就会提供一个IProductRepository的实例,控制器只定义了一个方法Index,这个方法返回数据库中所有的产品信息给默认视图
单元测试:Index Action
这个环节我们关心的是Index Action能正确的返回存储库中的所有产品对象,我可以创建一个模拟存储库来测试他返回的数据是否与测试的数据一致,在单元测试项目中新建一个叫做AdminControllerTests.cs的测试文件,其内容如下;
using Microsoft.AspNetCore.Mvc;
using Moq;
using SportsStore.Controllers;
using SportsStore.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace SportsStore.Tests
{
public class AdminControllerTests
{
[Fact]
public void Index_Contains_All_Products()
{
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product{ProductID=1,Name="P1"},
new Product{ProductID=2,Name="P2"},
new Product{ProductID=3,Name="P3"}
}.AsQueryable<Product>());
//Arrange - create a controller
AdminController target = new AdminController(mock.Object);
//Action
Product[] result =
GetViewModel<IEnumerable<Product>>(target.Index())?.ToArray();
//Assert
Assert.Equal(3, result.Length);
Assert.Equal("P1", result[0].Name);
Assert.Equal("P2", result[1].Name);
Assert.Equal("P3", result[2].Name);
}
private T GetViewModel<T>(IActionResult result) where T : class
{
return (result as ViewResult)?.ViewData.Model as T;
}
}
}
我增加了一个GetViewModel的方法用于解压缩Action方法返回的结果并得到查看的模型数据,我将在本章后面的部分添加更多的测试.
实现List视图
下一步是给Index方法增加一个视图,在Views/Admin文件夹下增加一个 Index.cshtml的Razor视图,内容如Listing 11-6 所示
Listing 11-6. The Contents of the Index.cshtml File in the Views/Admin Folder
@model IEnumerable<Product>
@{
ViewBag.Title = "All Products";
Layout = "_AdminLayout";
}
<table class=" table table-sm table-bordered table-striped">
<tr>
<th class=" text-right ">ID</th>
<th>Name</th>
<th class=" text-right ">Price</th>
<th class="text-center ">Actions</th>
</tr>
@foreach (var item in Model)
{
<tr>
<td class=" text-right ">@item.ProductID</td>
<td>@item.Name</td>
<td class=" text-right ">@item.Price.ToString("c")</td>
<td class=" text-center">
<form asp-action="Delete" method="post">
<a asp-action="Edit" class="btn btn-sm btn-warning"
asp-route-productID="@item.ProductID">Edit</a>
<input type="hidden" name="productID" value="@item.ProductID" />
<button type="submit" class=" btn btn-danger btn-sm">Delete</button>
</form>
</td>
</tr>
}
</table>
<div class="text-center">
<a asp-action="Create" class="btn btn-primary">Add Product</a>
</div>
此视图包含一个表,表中每行代表一个产品,单元格中包含产品名称,产品价格,并且包含一个指向Edit方法的Edit按钮和一个指向Delete方法的Delete按钮,在表格的下方,有一个指向Create方法的Add Product按钮,接下来我将在控制器内增加Edit,Delete,Create方法,你现在可以打开应用程序导航到Admin/Index URL看下当前显示的效果,如图Figure 11-3
提示:“编辑”按钮位于清单11-6中的表单元素内部,以便两个按钮彼此相邻,解决Bootstrap应用的间距问题。 “编辑”按钮将向服务器发送HTTP GET请求获取产品的当前详细信息; 这不需要表单元素。 但是,由于删除按钮会生成对应用程序状态的更改,我需要使用HTTP POST请求 - 这种情况应该使用表单元素。
Figure 11-3. Displaying the list of products
编辑产品(更改)
我增加了一个产品编辑页就像Figure 11-2 中显示的那样,用于支持产品的创建和修改,这可以分成两部分工作
允许管理员在页面上更改产品的额属性
增加一个方法用于处理用户的更改
创建编辑操作方法
创建的编辑方法如Listing 11-7 所示,他可以接受浏览器用户点击编辑按钮提交的http 请求
Listing 11-7. Adding an Edit Action Method in the AdminController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Products);
//-------------------------------------------------------------------------------
public ViewResult Edit(int productID) =>
View(repository.Products.FirstOrDefault(p => p.ProductID == productID));
//-------------------------------------------------------------------------------
}
}
这个方法很简单,就是根据参数传递过来的productID找到相关的存储库中的产品对象传给视图
单元测试:编辑方法
我们要测试编辑方法的两个方面,第一个是我可以根据我提供的可用ID得到我期望要编辑的产品,第二个是我提供一个存储库不存在的产品标志我应该得不到任何要编辑的产品,下面是我增加到AdminControllerTests.cs文件的测试方法
using Microsoft.AspNetCore.Mvc;
using Moq;
using SportsStore.Controllers;
using SportsStore.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;
namespace SportsStore.Tests
{
public class AdminControllerTests
{
[Fact]
public void Index_Contains_All_Products()
{
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product{ProductID=1,Name="P1"},
new Product{ProductID=2,Name="P2"},
new Product{ProductID=3,Name="P3"}
}.AsQueryable<Product>());
//Arrange - create a controller
AdminController target = new AdminController(mock.Object);
//Action
Product[] result =
GetViewModel<IEnumerable<Product>>(target.Index())?.ToArray();
//Assert
Assert.Equal(3, result.Length);
Assert.Equal("P1", result[0].Name);
Assert.Equal("P2", result[1].Name);
Assert.Equal("P3", result[2].Name);
}
private T GetViewModel<T>(IActionResult result) where T : class
{
return (result as ViewResult)?.ViewData.Model as T;
}
//--------------------------------------------------------------------------
[Fact]
public void Can_Edit_Product()
{
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product{ProductID=1,Name="P1"},
new Product{ProductID=2,Name="P2"},
new Product{ProductID=3,Name="P3"},
}.AsQueryable<Product>());
//Arrange - create the controller
AdminController target = new AdminController(mock.Object);
//Act
Product p1 = GetViewModel<Product>(target.Edit(1));
Product p2 = GetViewModel<Product>(target.Edit(2));
Product p3 = GetViewModel<Product>(target.Edit(3));
//Assert
Assert.Equal(1, p1.ProductID);
Assert.Equal(2, p2.ProductID);
Assert.Equal(3, p3.ProductID);
}
[Fact]
public void Cannot_Edit_Nonexistent_Product()
{
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product{ProductID=1,Name="P1"},
new Product{ProductID=2,Name="P2"},
new Product{ProductID=3,Name="P3"},
}.AsQueryable<Product>());
//Arrange - create the controller
AdminController target = new AdminController(mock.Object);
//Act
Product result = GetViewModel<Product>(target.Edit(4));
//Assert
Assert.Null(result);
}
//-----------------------------------------------------------------------
}
}
创建编辑视图
现在已经创建好动作方法,接下来创建视图,你可以这样操作(与原英文版有差异,但不失为一种快捷的方法),打开控制器文件,在方法名称上(Edit)右键点击鼠标,选择添加视图,然后会弹出一个对话框,不用管对话框的内容,直接点击添加,如下图所示
而后VS会自动创建好这个视图(在对应的文件夹内),我们编辑这个文件的内容如Listing 11-8 所示(原有的内容删除)
Listing 11-8. The Contents of the Edit.cshtml File in the Views/Admin Folder
@model Product
@{
ViewBag.Title = "Edit Product";
Layout = "_AdminLayout";
}
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="ProductID" />
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Description"></label>
<input asp-for="Description" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Category"></label>
<input asp-for="Category" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control" />
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Save</button>
<a asp-action="Index" class=" btn btn-secondary">Cancel</a>
</div>
</form>
该视图包含一个HTML表单,该表单使用标记帮助程序生成大部分内容,包括设置表单和元素的目标,设置标签元素的内容,并生成input和textarea元素的name,id和value属性。
可以启动应用程序 导航到/Admin/Index URL 并点击产品的编辑按钮 看一下运行效果,如图Figure 11-4
提示:为简单起见,我为ProductID属性使用了隐藏的输入元素,这样做的意义是当Entity Framework存储新对象时,ProductID是由数据库生成为主键核心,安全地改变它可能是一个复杂的过程(不能改变)。 对于大多数应用程序,最简单的方法是阻止用户更改他的值。
Figure 11-4. Displaying product values for editing
更新产品存储库
在我开始更新产品之前,我需要强化产品存储库使之可以保存变更,第一步,在IProductRepository接口中定义新的方法,如Listing 11-9 所示
Listing 11-9. Adding a Method to the IProductRespository.cs File in the Models Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SportsStore.Models
{
public interface IProductRepository
{
IQueryable<Product> Products { get; }
void SaveProduct(Product product);
}
}
然后增加一个方法实现这个接口,在EFProductRepository.cs文件中修改如Listing 11-10
Listing 11-10. Implementing the New Method in the EFProductRepository.cs File in the Models Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SportsStore.Models
{
public class EFProductRepository : IProductRepository
{
private ApplicationDbContext context;
public EFProductRepository(ApplicationDbContext ctx)
{
context = ctx;
}
public IQueryable<Product> Products => context.Products;
//-----------------------------------------------------------------------------
public void SaveProduct(Product product)
{
if (product.ProductID == 0)
{
context.Products.Add(product);
}
else
{
Product dbEntry = context.Products.FirstOrDefault(p => p.ProductID == product.ProductID);
if (dbEntry != null)
{
dbEntry.Name = product.Name;
dbEntry.Description = product.Description;
dbEntry.Price = product.Price;
dbEntry.Category = product.Category;
}
context.SaveChanges();
}
}
//----------------------------------------------------------------------------------
}
}
这个方法先判定接受的产品ID.如果为0 说明是一个新增产品,反之,则对响应的产品进行变更并存储都数据库中
不要纠结于实体框架细节,我之前说过,他并不是asp.net core mvc的一部分,它自己可以单独拉出来详细探索,我们关注这个方法中MVC的程序设计方式即可
我们接受一个参数productID 首先确认它不为0,然后去存储库中找到跟他ProductID相同的那个产品对象,并把他的对应的属性变更成参数传递过来的响应值即可,
之所以这样做是因为实体框架会跟踪它从数据库创建的对象,传递给savechanges方法的对象都由mvc模型绑定功能创建的,这意味着实体框架对于新对象一无所知也不会对数据库作出变更,有很多方法可以解决这个问题,我们采取最简单一种方式,这就是我们上面代码中所使用的 根据ProductID找到存储库中相关的产品对象然后更新它.<这段话我感觉就是废话>
在IProductRepository接口中新增的方法已经使得虚拟的存储库被破坏,因为它是继承于这个接口的,所以它应该实现所有接口中的方法,但是它没有,这样就无法通过编译,我在第8章创建这个虚拟存储库是为了演示如何无缝的由虚拟存储库切换到真实的实现而不用修改依赖它们的组件,接下来虚拟存储库就不在使用了,它已经完成了它的任务,为了编译通过,可以在虚拟存储库中删除对该接口的继承,以便我们在完成接口功能时不需要再修改此类,如Listing 11-11
Listing 11-11. Removing the Interface in the FakeProductRepository.cs File in the Models Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SportsStore.Models
{
//----------------------------------------------------------
public class FakeProductRepository //: IProductRepository
//----------------------------------------------------------
{
public IQueryable<Product> Products
{
get
{
return new List<Product>
{
new Product{Name="Foot ball",Price=25},
new Product{Name="Surf board",Price=179},
new Product{Name="Running shoes",Price=95}
}.AsQueryable<Product>();
}
}
}
}
处理编辑请求
我在控制器类中实现了处理当用户点击Save按钮时的请求的Edit动作方法,它是采用[httppost]修饰的重载方法,如Listing 11-12所示
Listing 11-12. Defining an Action Method in the AdminController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Products);
public ViewResult Edit(int productID) =>
View(repository.Products.FirstOrDefault(p => p.ProductID == productID));
//------------------------------------------------------------------------
[HttpPost]
public IActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
repository.SaveProduct(product);
TempData["message"] = $"{product.Name} has been saved";
return RedirectToAction("Index");
}
else
{
//there is something wrong with the data values
return View(product);
}
}
//-------------------------------------------------------------------------
}
}
我通过读取ModelState.Isvalid属性的值来验证用户提交的数据是否符合绑定的模型,如果一切正常,我就把数据保存到存储库并重新定向到Index视图,以便他们查看已经修改的产品信息,如果验证不通过,就返回修改产品的视图,以便用户检查是不是哪里输错了,并进行更正,
当我保修修改后,我用TempData存储了一个信息,TempData是Session特性的一部分,这个键值对字典与我之前用过的viewbag特性相似,不同在于在我使用tempdate数据之前这个数据是一直在的(可以跨视图使用);
我不能在这种情况下使用ViewBag,因为他不能跨视图使用,他只能在当前控制器方法与对应视图时间传递数据,本例中,当编辑成功后,页面被重定向到新的URL,因此ViewBag的数据就丢失掉了,我可以使用回话数据功能,但是随后数据会一直存在,直到我明确的删除它,这样就增加了一个步骤,显然使用TempData是最好的(一旦使用后他就自行删除了,TempData的特点就是只能使用一次)
因此,使用临时数据功能非常适合数据仅限于单个用户的会话(以便用户没有看到其他人的TempData),TempData会持续足够长的时间让我阅读它。 我将读取数据由我重定向用户的操作方法呈现的视图,我在下一节中定义。
单元测试:提交更改
对于提交修改的方法,我需要测试两个方面,一个是可用的更新要确保已经把数据保存到存储库中,另一个是无效的更新应确保不能保存到存储库中,具体测试代码如下,把下列代码增加到AdminControllerTests.cs中
[Fact]
public void Can_Save_Valid_Changes()
{
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
//Arrange - create mock temp data
Mock<ITempDataDictionary> tempData = new Mock<ITempDataDictionary>();
//Arrange - create the controller
AdminController target = new AdminController(mock.Object)
{
TempData = tempData.Object
};
//Arrange - create a product
Product product = new Product { Name = "Test" };
//Act - try to save the product
IActionResult result = target.Edit(product);
//Assert - check that the repository was called
mock.Verify(m => m.SaveProduct(product));
//Assert - check the result type is a redirection
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Index", (result as RedirectToActionResult).ActionName);
}
[Fact]
public void Cannot_Save_Invalid_Changes()
{
// Arrange - create mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
// Arrange - create the controller
AdminController target = new AdminController(mock.Object);
// Arrange - create a product
Product product = new Product { Name = "Test" };
// Arrange - add an error to the model state
target.ModelState.AddModelError("error", "error");
// Act - try to save the product
IActionResult result = target.Edit(product);
// Assert - check that the repository was not called
mock.Verify(m => m.SaveProduct(It.IsAny<Product>()), Times.Never());
// Assert - check the method result type
Assert.IsType<ViewResult>(result);
}
显示确认信息
我将在_AdminLayout.cshtml布局文件中处理我使用TempData存储的消息,如清单 Listing 11-13所示。 通过处理模板中的消息,我可以在任何使用的视图中创建消息模板,无需创建其他Razor表达式。
Listing 11-13. Handling the ViewBag Message in the _AdminLayout.cshtml File
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
<title>@ViewBag.Title</title>
</head>
<body class="m-1 p-1">
<div class=" bg-info p-2">
<h4>@ViewBag.Title</h4>
</div>
//-----------------------------------------------------
@if (TempData["message"] != null)
{
<div class=" alert alert-success">@TempData["message"]</div>
}
//------------------------------------------------------
@RenderBody()
</body>
</html>
提示:像这样处理模板中的消息的好处是用户将看到它显示在上面保存更改后呈现的任何页面。 目前,我将它们退回产品列表,但我可以更改工作流程以呈现其他视图,用户仍然会看到该消息(只要下一个视图也使用相同的布局)。
我现在已经准备好了编辑产品的所有部分。 要查看它是如何工作的,请启动应用程序,导航到/ Admin / Index URL,单击“编辑”按钮,然后进行更改。 单击“保存”按钮。 你将会重定向到/ Admin / Index URL,将显示TempData消息,如图 Figure 11-5所示。如果重新加载产品列表屏幕,该消息将消失,因为TempData在读取时会被删除。这很方便,因为我不希望遗留旧信息。
Figure 11-5. Editing a product and seeing the TempData message
增加模型验证
我现在需要增加模型类验证规则了。 此刻,管理员可以输入负价或空白描述,而SportsStore会很乐意存储它到数据库中。 这样的错误数据是否会成功保留将取决于是否它符合Entity Framework Core创建的SQL表中的约束,但这还不够保护大多数应用程序。 为了防止错误的数据值,我修饰了产品的属性,如清单Listing 11-14所示,就像我在第10章中对Order类所做的那样。
Listing 11-14. Applying Validation Attributes in the Product.cs File in the Models Folder
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace SportsStore.Models
{
public class Product
{
public int ProductID { get; set; }
[Required(ErrorMessage ="please enter a product name")]
public string Name { get; set; }
[Required(ErrorMessage ="please enter a product description")]
public string Description { get; set; }
[Required]
[Range(0.01,double.MaxValue,ErrorMessage ="please enter a positive value")]
public decimal Price { get; set; }
[Required(ErrorMessage ="please specify a category")]
public string Category { get; set; }
}
}
在第10章中,我使用了一个标记帮助程序来在表单顶部显示验证错误的摘要。对于这个例子,我将使用类似的方法,但我将在每个属性(文本框)旁边显示错误消息,如清单Listing 11-15所示。
Listing 11-15. Adding Validation Error Elements in the Edit.cshtml File in the Views/Admin Folder
@model Product
@{
ViewBag.Title = "Edit Product";
Layout = "_AdminLayout";
}
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="ProductID" />
<div class="form-group">
<label asp-for="Name"></label>
//------------------------------------------------------------------------
<div><span asp-validation-for="Name" class=" text-danger"></span></div>
//---------------------------------------------------------------------------
<input asp-for="Name" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Description"></label>
//-------------------------------------------------------------------------
<div><span asp-validation-for="Description" class=" text-danger"></span></div>
//-----------------------------------------------------------------------
<input asp-for="Description" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Category"></label>
//-----------------------------------------------------------------
<div><span asp-validation-for="Category" class=" text-danger"></span></div>
//------------------------------------------------------------------
<input asp-for="Category" class="form-control" />
</div>
<div class="form-group">
<label asp-for="Price"></label>
//--------------------------------------------------------------------
<div><span asp-validation-for="Price" class=" text-danger"></span></div>
//---------------------------------------------------------------------
<input asp-for="Price" class="form-control" />
</div>
<div class="text-center">
<button class="btn btn-primary" type="submit">Save</button>
<a asp-action="Index" class=" btn btn-secondary">Cancel</a>
</div>
</form>
当应用于span元素时,asp-validation-for属性应用将添加a的标记助手如果存在任何验证问题,则指定属性的验证错误消息。标记助手将向span元素中插入一条错误消息,并将该元素添加到inputvalidation-错误类,可以很容易地将CSS样式应用于错误消息元素,如图所示清单 Listing 11-16。
Listing 11-16. Adding CSS to the _AdminLayout.cshtml File in the Views/Shared Folder
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
<title>@ViewBag.Title</title>
//----------------------------------------------------------------
<style>
.input-validation-error {
border-color: red;
background-color: #fee;
}
</style>
//----------------------------------------------------------------
</head>
<body class="m-1 p-1">
<div class=" bg-info p-2">
<h4>@ViewBag.Title</h4>
</div>
@if (TempData["message"] != null)
{
<div class=" alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</body>
</html>
我定义的CSS样式选择为input-validation-error类的成员元素应用红色边框和背景颜色。
提示:使用像Bootstrap这样的CSS库时显式设置样式会导致内容与应用主题不一致。 在第27章中,我展示了一种使用JavaScript代码替代Bootstrap的方法来呈现具有验证错误的元素的类,这使得所有内容保持一致,但也更复杂.
您可以在视图中的任何位置应用验证消息标记帮助程序,但常规的做法是把它放在问题元素的附近,给用户一些明显的提示。 如图 Figure 11-6所示,要查看显示的验证消息和提示的效果,您可以通过运行应用程序,点击编辑产品来查看,并提交无效数据。
Figure 11-6. Data validation when editing products
使用客户端验证
当前,数据验证功能只能在管理员把数据提交到服务器端才能生效,但是大部分人希望在他们输入了错误的信息(不合法)时能得到及时反馈,这就是大部分开发者都使用客户端验证的原因,顾名思义,使用客户端验证是采用JavaScript来验证数据的,它可以根据我在领域模型中定义的数据验证规则来执行客户端验证,
第一步是要增加客户端验证的JavaScript库,更改bower.json如清单 Listing 11-17 所示(也可以鼠标右键单击bower.json文件,选择管理bower程序包,然后安装最新版本即可)
Listing 11-17. Adding JavaScript Packages in the bower.json File
{
"name": "asp.net",
"private": true,
"dependencies": {
"bootstrap": "v4.0.0-alpha.6",
"font-awesome": "Font-Awesome#v4.7.0",
"jquery-validation": "1.16.0",
"jquery-validation-unobtrusive": "v3.2.6",
"jQuery": "3.2.0"
}
}
客户端验证建立在流行的jQuery库之上,这简化了使用浏览器的DOM API的难度。 下一步是将JavaScript文件添加到布局中,以便我们在使用SportsStore管理功能的时候加载它们.如清单 Listing 11-18 所示
Listing 11-18. Adding the Validation Libraries to the _AdminLayout.cshtml File in the Views/Shared Folder
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
//-------------------------------------------------------------------------------
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
//-----------------------------------------------------------------------------
<title>@ViewBag.Title</title>
<style>
.input-validation-error {
border-color: red;
background-color: #fee;
}
</style>
</head>
<body class="m-1 p-1">
<div class=" bg-info p-2">
<h4>@ViewBag.Title</h4>
</div>
@if (TempData["message"] != null)
{
<div class=" alert alert-success">@TempData["message"]</div>
}
@RenderBody()
</body>
</html>
启用客户端验证不会导致任何可视更改,但会引起约束应用于C#模型类的属性在浏览器中强制执行验证,并阻止用户提交包含错误数据的表单,即出现问题时提供即时反馈的功能.可以学习第27章了解更多内容,
创建新的产品
接下来,我们去实现Create动作方法,这就可以让管理员在产品页添加新的产品内容,我们只需要在程序中增加一点内容即可使它具备增加产品的能力,这是一个很好的例子来展示具有良好结构的mvc程序具有强大的功能和灵活性,第一步,添加一个Create动作方法,如清单Listing 11-19 所示,
Listing 11-19. Adding the Create Action to the AdminController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Products);
public ViewResult Edit(int productID) =>
View(repository.Products.FirstOrDefault(p => p.ProductID == productID));
[HttpPost]
public IActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
repository.SaveProduct(product);
TempData["message"] = $"{product.Name} has been saved";
return RedirectToAction("Index");
}
else
{
//there is something wrong with the data values
return View(product);
}
}
//-------------------------------------------------------------------
public ViewResult Create() => View("Edit", new Product());
//-------------------------------------------------------------------
}
}
这个Create动作方法没有单独生成它自己的对应视图,而是采用了Edit动作方法中创建的视图,通常一个操作方法使用另一个与之相关联的视图是完全可以接受的。 这里,我提供了一个新的Product对象作为视图模型,以便使用空字段填充Edit视图。
提示:我没有为Create增加单元测试,如果增加也只会测试ASP.NET CoreMVC处理动作方法结果的结果的能力,这是mvc框架理所当然的能力。(除非您怀疑存在缺陷,否则通常不会为框架功能编写测试。)
这是唯一需要的更改,因为“编辑”操作方法已设置为接收来自模型绑定系统的产品对象并将它们存储在数据库中。 你可以测试一下效果,启动应用程序,导航到/ Admin / Index,单击添加产品按钮,然后启用功能填写并提交表单。 您在表单中指定的详细信息将用于创建新产品在数据库中,它将出现在列表中,如图 Figure11-7所示。
Figure 11-7. Adding a new product to the catalog
删除产品
增加删除功能也很简单,首先给IProductRepository接口增加一个删除声明,如清单Listing 11-20 所示
Listing 11-20. Adding a Method to Delete Products to the IProductRepository.cs File in the Models Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SportsStore.Models
{
public interface IProductRepository
{
IQueryable<Product> Products { get; }
void SaveProduct(Product product);
//--------------------------------------------------
void DeleteProduct(int productID);
//---------------------------------------------------
}
}
下一步是在EF产品存储库中实现这个方法,在EFProductRepository.cs中修改如清单 Listing 11-21 所示
Listing 11-21. Implementing Deletion Support in the EFProductRepository.cs File in the Models Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SportsStore.Models
{
public class EFProductRepository : IProductRepository
{
private ApplicationDbContext context;
public EFProductRepository(ApplicationDbContext ctx)
{
context = ctx;
}
public IQueryable<Product> Products => context.Products;
public void SaveProduct(Product product)
{
if (product.ProductID == 0)
{
context.Products.Add(product);
}
else
{
Product dbEntry = context.Products.FirstOrDefault(p => p.ProductID == product.ProductID);
if (dbEntry != null)
{
dbEntry.Name = product.Name;
dbEntry.Description = product.Description;
dbEntry.Price = product.Price;
dbEntry.Category = product.Category;
}
}
context.SaveChanges();
}
//---------------------------------------------------------------------
public Product DeleteProduct(int productID)
{
Product dbEntry = context.Products.FirstOrDefault(p =>
p.ProductID == productID);
if (dbEntry != null)
{
context.Products.Remove(dbEntry);
context.SaveChanges();
}
return dbEntry;
}
//-------------------------------------------------------------------
}
}
最后一步是在Admin控制器中实现删除的动作方法,这个删除动作方法仅支持POST请求,因为删除是提交的表单的post方式,就像我在第16章解释的,浏览器和缓存是可以发出get请求的,所以我们应该十分小心的避免这个删除是由一个get请求发出的,清单 Listing 11-22 展示了我新增的Delete 动作方法
Listing 11-22. Adding the Delete Action Method in the AdminController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class AdminController : Controller
{
private IProductRepository repository;
public AdminController(IProductRepository repo)
{
repository = repo;
}
public ViewResult Index() => View(repository.Products);
public ViewResult Edit(int productID) =>
View(repository.Products.FirstOrDefault(p => p.ProductID == productID));
[HttpPost]
public IActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
repository.SaveProduct(product);
TempData["message"] = $"{product.Name} has been saved";
return RedirectToAction("Index");
}
else
{
//there is something wrong with the data values
return View(product);
}
}
public ViewResult Create() => View("Edit", new Product());
//--------------------------------------------------------------------------
[HttpPost]
public IActionResult Delete(int productID)
{
Product deleteProduct = repository.DeleteProduct(productID);
if (deleteProduct != null)
{
TempData["message"] = $"{deleteProduct.Name} was deleted";
}
return RedirectToAction("Index");
}
//----------------------------------------------------------------------------
}
}
单元测试:删除产品
我想测试Delete动作方法的基本行为,即通过参数传递有效的ProductID时action方法调用存储库的DeleteProduct方法
删除对应的Product。 这是我添加到的测试AdminControllerTests.cs文件:
[Fact]
public void Can_Delete_Valid_Products()
{
//Arrange - create a product
Product prod = new Product { ProductID = 2, Name = "Test" };
//Arrange - create the mock repository
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[]
{
new Product{ProductID=1,Name="P1"},
prod,
new Product{ProductID=3,Name="P3"},
}.AsQueryable<Product>());
//Arrange - create the controller
AdminController target = new AdminController(mock.Object);
//Act - delete the product
target.Delete(prod.ProductID);
//Assert - ensure that the repository delete method was called with the correct Product
mock.Verify(m => m.DeleteProduct(prod.ProductID));
}
您可以通过启动应用程序,导航到/ Admin / Index并单击一个产品的删除按钮来查看删除功能,如图Figure 11-8所示。
Figure 11-8. Deleting a product from the catalog
注意:如果删除之前创建的产品,则可能因此订单错误,当Order对象存储在数据库中时,它将转换为数据库表中的条目
包含与其关联的Product对象的引用,称为外键关系。默认情况下,数据库不允许删除在Order订单中引用的Product对象,因为这样做会在数据库中产生不一致, 有一些解决此问题的方法,包括在相关产品时自动删除含有这个产品的Order对象或者更改Product和Order对象之间的关系。 请参阅实体框架核心文档详细信息。
总结
在本章中,我介绍了管理功能,并向您展示了如何实现CRUD允许管理员从存储库创建,读取,更新和删除产品的操作将订单标记为已发货。 在下一章中,我将向您展示如何保护管理功能,虽然这样做并不适用于所有用户,同时最终我将SportsStore应用程序部署到生产环境中。
本章到此结束
2019年2月9日09:37:43 星期六 家