3 创建购物车
每个商品旁边都要显示Add to cart按钮。点击按钮后,会显示客户已经选中的商品的摘要,包括总金额。在购物车里,用户可以点击继续购物按钮返回product目录。也可以点击Checkout now按钮,完成订单和购物会话。
3.1 定义Cart Entity
购物车是程序业务域的一部分,在我们的领域模型中创建实体。添加一个Cart类到Entities文件夹。
1 namespace SportsStore.Domain.Entities 2 { 3 publicclass Cart 4 { 5 private List<CartLine> lineCollection =new List<CartLine>(); 6 7 publicvoid AddItem(Product product, int quantity) 8 { 9 //检查购物车中是否已经有该产品10 CartLine line = lineCollection 11 .Where(p => p.Product.ProductID == product.ProductID) 12 .FirstOrDefault(); 13 14 if (line ==null) 15 { 16 lineCollection.Add(new CartLine { Product = product, Quantity = quantity }); 17 } 18 else19 { 20 line.Quantity +=quantity; 21 } 22 } 23 24 publicvoid RemoveLine(Product product) 25 { 26 lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID); 27 } 28 29 publicdecimal ComputeTotalValue() 30 { 31 return lineCollection.Sum(e => e.Product.Price * e.Quantity); 32 } 33 34 publicvoid Clear() 35 { 36 lineCollection.Clear(); 37 } 38 39 public IEnumerable<CartLine> Lines 40 { 41 get { return lineCollection; } 42 } 43 } 44 publicclass CartLine 45 { 46 public Product Product { get; set; } 47 publicint Quantity { get; set; } 48 } 49 } 50
购物车类使用CartLine,代表用户选中的一个商品。定义了添加、移除、计算合计、清空的方法。我们也提供了一个属性,返回IEnumerble<CartLine。
3.1.1 测试购物车单元测试
Cart类相对简单,但有一些非常重要的行为,我们必须确保工作正常。一个功能不良的购物车会破坏程序的整体。偶们对这些特性一个一个测试。
第一个要测试的行为,是将添加货物到购物车。如果该商品是第一次被加到购物车,我们需要一个新的CartLine。
1 [TestMethod] 2 publicvoid Can_Add_New_Lines() 3 { 4 //Arrange - create some test products 5 Product p1 =new Product { ProductID =1, Name ="P1" }; 6 Product p2 =new Product { ProductID =2, Name ="P2" }; 7 8 //Arrange - create a new cart 9 Cart target =new Cart(); 10 11 //Act12 target.AddItem(p1, 1); 13 target.AddItem(p2, 1); 14 CartLine[] results = target.Lines.ToArray(); 15 16 //Assert17 Assert.AreEqual(results.Length, 2); 18 Assert.AreEqual(results[0].Product, p1); 19 Assert.AreEqual(results[1].Product, p2); 20 }
如果客户已经添加过该商品,我们需要增加相应CartLine的数量,而不是创建一个新的。
1 [TestMethod] 2 publicvoid Can_Add_Quantiy_For_Existing_Lines() 3 { 4 //Arrange - create some test products 5 Product p1 =new Product { ProductID =1, Name ="P1" }; 6 Product p2 =new Product { ProductID =2, Name ="P2" }; 7 8 //Arrange - create a new cart 9 Cart target =new Cart(); 10 11 //Act12 target.AddItem(p1, 1); 13 target.AddItem(p2, 1); 14 target.AddItem(p1, 10); 15 CartLine[] results = target.Lines.OrderBy(c => c.Product.ProductID).ToArray(); 16 17 //Assert18 Assert.AreEqual(results.Length, 2); 19 Assert.AreEqual(results[0].Quantity, 11); 20 Assert.AreEqual(results[1].Quantity, 1); 21 }
我们也需要检查移除商品的功能。
1 [TestMethod] 2 publicvoid Can_Remove_Lines() 3 { 4 //Arrange - create some test products 5 Product p1 =new Product { ProductID =1, Name ="P1" }; 6 Product p2 =new Product { ProductID =2, Name ="P2" }; 7 Product p3 =new Product { ProductID =3, Name ="P3" }; 8 9 //Arrange - create a new cart10 Cart target =new Cart(); 11 12 //Arrange - add some products to the cart13 target.AddItem(p1, 1); 14 target.AddItem(p2, 3); 15 target.AddItem(p3, 5); 16 target.AddItem(p2, 1); 17 18 //Act19 target.RemoveLine(p2); 20 21 //Assert22 Assert.AreEqual(target.Lines.Where(c=>c.Product==p2).Count(),0); 23 Assert.AreEqual(target.Lines.Count(),2); 24 }
计算总金额的功能:
1 [TestMethod] 2 publicvoid Calculate_Cart_Total() 3 { 4 //Arrange - create some test products 5 Product p1 =new Product { ProductID =1, Name ="P1" ,Price=100M}; 6 Product p2 =new Product { ProductID =2, Name ="P2" ,Price=50M}; 7 8 //Arrange - create a new cart 9 Cart target =new Cart(); 10 11 //Act12 target.AddItem(p1, 1); 13 target.AddItem(p2, 1); 14 target.AddItem(p1, 3); 15 decimal result = target.ComputeTotalValue(); 16 17 //Assert18 Assert.AreEqual(result, 450M); 19 }
最后测试的是清空功能
1 [TestMethod] 2 publicvoid Can_Clear_Contents() 3 { 4 //Arrange - create some test products 5 Product p1 =new Product { ProductID =1, Name ="P1" ,Price=100M}; 6 Product p2 =new Product { ProductID =2, Name ="P2" ,Price=50M}; 7 8 //Arrange - create a new cart 9 Cart target =new Cart(); 10 11 //Act12 target.AddItem(p1, 1); 13 target.AddItem(p2, 1); 14 15 target.Clear(); 16 17 //Assert18 Assert.AreEqual(target.Lines.Count(), 0); 19 }
3.2 Add to Cart按钮
1 @model SportsStore.Domain.Entities.Product 2 3 <div class="item"> 4 <h3>@Model.Name</h3> 5 @Model.Description 6 7 @using(Html.BeginForm("AddToCart","Cart")){ 8 @Html.HiddenFor(x=>x.ProductID) 9 @Html.Hidden("returnUrl",Request.Url.PathAndQuery) 10 <input type="submit" value="+ Add to cart"/>11 } 12 13 <h4>@Model.Price.ToString("c")</h4>14 </div>
改变ProductSummary.cshtml局部视图。当表单被提交时,会提交到Cart controller中的AddToCart action方法。
默认地,BeginForm helper方法创建一个表单,使用HTTP POST方法。可以变为GET方法。
3.2.1 在同一个地方创建多个HTML FORMS
使用HTML.BeginForm helper在每个商品列表,意味着每个Add to cart按钮会被渲染在相互分隔的自己的HTML from元素中。在ASP.NET Web Forms中,一个页面限制只有一个form。ASP.NET MVC不限制每个页面的form数量,你要多少可以加多少。
不同form返回到相同的controller方法,伴随着不同的参数值,这是一个很好而且简单的方法,来处理button点击。
3.3 实现Cart Controller
我们需要创建一个CartController,来处理Add to cart按钮的点击。
1 publicclass CartController : Controller 2 { 3 // 4 // GET: /Cart/ 5 private IProductRepository repository; 6 7 public CartController(IProductRepository repo) 8 { 9 repository = repo; 10 } 11 12 public RedirectToRouteResult AddToCart(int productId, string returnUrl) 13 { 14 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 15 16 if (product !=null) 17 { 18 GetCart().AddItem(product, 1); 19 } 20 return RedirectToAction("Index", new { returnUrl }); 21 } 22 23 public RedirectToRouteResult RemoveFromCart(int productId,string returnUrl) 24 { 25 Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); 26 27 if(product!=null){ 28 GetCart().RemoveLine(product); 29 } 30 return RedirectToAction("Index", new { returnUrl }); 31 32 } 33 34 private Cart GetCart() 35 { 36 Cart cart = (Cart)Session["Cart"]; 37 if(cart==null){ 38 cart =new Cart(); 39 Session["Cart"] = cart; 40 } 41 return cart; 42 } 43 }
这里有几个点。第一个是ASP.NET session状态特性,存储并检索Cart对象,这是GetCart方法的目的。ASP.NET有很好的session特性,使用cookis或URL重写用户的关联请求,从form一个单一浏览session。相关的的特性是session状态,它允许我们用session关联数据。这是一个适合偶们Cart类的想法。偶们像让每个用户有自己的购物车,我们想让购物车固定在不同的请求。数据关联到session,session过期时会删除。这意味着我们不需要管理Cart类的存储或生命周期。
1 Session["Cart"]=cart;//在Session对象上设置一个key的value2 Cart cart=(Cart)Session["Cart"];//检索对象,读取key
Session装填对象,默认存储在Asp.net服务器的内存中。你可以配置一个不同的存储路径,包括使用Sql数据库。
在AddToCart和RemoveFromCart方法中,我们使用参数名匹配HTML form中输入的元素。这允许MVC框架关联POST变量传递来的参数,意味着我们不需要手动处理。
3.4 显示Cart的Content
RedirectToAction方法,它的效果是,发送一个HTTP重定向指令,到客户端浏览器,让浏览器请求一个新的URL。在这个例子中,我们让浏览器请求Cart controller的Index action。
我们会实现Index方法,用它播放Cart的contents。偶们需要传递两个信息碎片给view:Cart对象和如果用户点击继续购物按钮后要显示的URL。为了这个目的,我们会创建一个简单的视图模型类,CartIndexViewModel。
1 publicclass CartIndexViewModel 2 { 3 public Cart Cart { get; set; } 4 publicstring ReturnUrl { get; set; } 5 }
然后在CartController中添加Index方法
1 public ViewResult Index(string returnUrl) 2 { 3 return View(new CartIndexViewModel 4 { 5 Cart = GetCart(), 6 ReturnUrl = returnUrl 7 }); 8 }
并使用CartIndexViewModel(SportsStore.WebUI.Models)创建强类型视图。我们想在显示cart的content时,一如既往地与程序的其他部分页面一样,所以没有填layout,它会默认地使用_Layout.cshtml文件。
1 @model SportsStore.WebUI.Models.CartIndexViewModel 2 3 @{ 4 ViewBag.Title ="Sport Store : Your Cart"; 5 } 6 7 <h2>Your cart</h2> 8 <table width="90%" align="center"> 9 <thead>10 <tr>11 <th align="center">Quantity</th>12 <th align="left">Item</th>13 <th align="right">Price</th>14 <th align="right">Subtotal</th>15 </tr></thead>16 <tbody>17 @foreach(var line in Model.Cart.Lines){ 18 <tr>19 <td align="center">@line.Quantity</td>20 <td align="left">@line.Product.Name</td>21 <td align="right">@line.Product.Price.ToString("c")</td>22 <td align="right">@((line.Quantity*line.Product.Price).ToString("c"))</td>23 </tr>24 } 25 </tbody>26 <tfoot>27 <tr>28 <td colspan="3" align="right">Total:</td>29 <td align="right">30 @Model.Cart.ComputeTotalValue().ToString("c") 31 </td>32 </tr>33 </tfoot>34 </table>35 <p align-"center"class="actionButtons">36 <a href="@Model.ReturnUrl">Continue shopping</a>37 </p>38
它枚举购物车中的行,将每行添加到HTML表,伴随着每行总额和购物车总额。当我们点击继续购物按钮,会回到来时的页面。
4 使用模型绑定
MVC框架使用一个叫做model binding的系统,从来自HTTP查询,创建C#对象,为了将他们作为参数值传递给action方法。这是MVC如何处理表单。框架查看被触发的action方法的参数,并使用一个model binder,得到表单input元素的值,并使用相同的名字,将他们转换为参数的类型。
Model binders可以从有效查询的任何信息创建C#类型。这是MVC框架的中心特性之一。我们要创建一个自定义的模型绑定,来改进CartController类。
我们喜欢使用session状态特性,来存储和管理Cart对象。但是我们确实不喜欢这种方式。它不适合我们其他部分的程序模型,那些基于action方法参数。我们不能在CartController类使用单元测试,除非我们Mock Session,这意味着mocking真个controller类。
为了解决这个问题,我们要创造一个自定义model binder,获得session data中包含的cart对象。MVC框架然后会创建Cart对象,传递他们作为参数给action方法。模型绑定特性是非常强大和灵活的。
4.1 创建自定义Model Binder
我们创建自定义model binder,以实现IModelBinder接口。在SportsStore.WebUI中新建Binders文件,在它里面创建CartModelBinder类。
1 publicclass CartModelBinder:IModelBinder 2 { 3 privateconststring sessionKey ="Cart"; 4 5 publicobject BindModel(ControllerContext controllerContext,ModelBindingContext bindingContext) 6 { 7 //get the Cart from the session 8 Cart cart = (Cart)controllerContext.HttpContext.Session[sessionKey]; 9 //create the Cart if there wasn't one in the session data10 if(cart==null){ 11 cart =new Cart(); 12 controllerContext.HttpContext.Session[sessionKey] = cart; 13 } 14 //return the cart15 return cart; 16 } 17 }
IModelBinder接口定义了一个方法:BindModel。两个参数用来创建领域模型对象。ControllerContext提供访问controller拥有的所有信息,包括客户端的查询详情。ModelBindingContext给你关于你将要构建的模型对象的信息。
出于这个目的,ControllerContext类是我们感兴趣的。它由HttpContext属性,它可以给我们sesson属性,并设置session data。偶们通过读取session data的key的value,获得Cart,如果它不存在,就创建它。
偶们需要告诉MVC框架,它可以使用CartModelBinder类,创建Cart的实例。在Global.asax的Application_Start中添加
1 ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
现在我们可以将GetCart从CartController中移除,并使用我们的模型绑定。
1 public ViewResult Index(Cart cart,string returnUrl) 2 { 3 return View(new CartIndexViewModel 4 { 5 Cart = cart, 6 ReturnUrl = returnUrl 7 }); 8 }
我们移除了GetCart方法,并为每个action方法添加了Cart参数。当MVC框架收到请求,AddToCart方法被调用,它开始查找action方法的参数。它查看可用绑定的列表,尝试着找到一个能创建参数类型的实例。我们自定义的绑定,被要求创建一个Cart对象,它使用session状态特性完成工作。在我们的绑定和默认绑定之间,MVC框架会创建一组调用action方法必须的参数。允许我们重构controller。
使用自定义绑定有一些益处。第一,偶们分离了用来创建Cart的逻辑,从Controller。它允许偶们改变我们存储Cart对象的方法,而不需要改变controller。第二,用到Cart对象的任何Controller类,都能简单地将他们声明为action的参数,并改进自定义模型绑定。第三,是最重要的一点,偶们可以对Cartcontroller进行单元测试了,而不需要mock许多ASP.NET管道。
4..2 使用单元测试cart controller
通过创建Cart对象,并将他们传递给action方法,我们可以测试CarController类。需要测试controller的三个不同的方面:
- AddToCart方法应该添加被选择的product到用户的cart
- 在添加product到cart后,需要重定向到Index View
- 用户返回到分类的url,必须准确地传递给Index action方法
1 [TestMethod] 2 publicvoid Can_Add_To_Cart() 3 { 4 //Arrange - create the mock repository 5 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 6 mock.Setup(m => m.Products).Returns( 7 new Product[] 8 { 9 new Product{ProductID=1,Name="P1",Category="Apples"} 10 }.AsQueryable()); 11 12 //Arrange - create a Cart13 Cart cart =new Cart(); 14 15 //Arragne - create the controller16 CartController target =new CartController(mock.Object); 17 18 //Act - add a product to the cart19 target.AddToCart(cart, 1, null); 20 21 //Assert22 Assert.AreEqual(cart.Lines.Count(), 1); 23 Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1); 24 } 25 26 [TestMethod] 27 publicvoid Adding_Product_To_Cart_Goes_To_Cart_Screen() 28 { 29 //Arrange - create the mock repository30 Mock<IProductRepository> mock =new Mock<IProductRepository>(); 31 mock.Setup(m => m.Products).Returns( 32 new Product[] 33 { 34 new Product{ProductID=1,Name="P1",Category="Apples"} 35 }.AsQueryable()); 36 37 //Arrange - create a Cart38 Cart cart =new Cart(); 39 40 //Arragne - create the controller41 CartController target =new CartController(mock.Object); 42 43 //Act - add a product to the cart44 RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl"); 45 46 //Assert47 Assert.AreEqual(result.RouteValues["action"],"Index"); 48 Assert.AreEqual(result.RouteValues["returnUrl"],"myUrl"); 49 } 50 51 [TestMethod] 52 publicvoid Can_View_Cart_Contents() 53 { 54 //Arrange - create a Cart55 Cart cart =new Cart(); 56 57 //Arragne - create the controller58 CartController target =new CartController(null); 59 60 //Act - call the Index action method61 CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model; 62 63 //Assert64 Assert.AreEqual(result.Cart,cart); 65 Assert.AreEqual(result.ReturnUrl,"myUrl"); 66 } 67 }
5 完成购物车
添加两个心的特性,第一个是移除商品,第二个是在页面顶部显示商品总数
5.1 从购物车移除商品
我们已经定义并而是了RemoveFromCart action方法,需要把它放到视图,在购物车汇总的每一行添加Remove按钮。
1 <td align="right">@((line.Quantity*line.Product.Price).ToString("c"))</td>2 <td>3 @using(Html.BeginForm("RemoveFromCart","Cart")){ 4 @Html.Hidden("ProductId",line.Product.ProductID) 5 @Html.HiddenFor(x=>x.ReturnUrl) 6 <input class="actionButtons" type="submit" value="Remove"/>7 } 8 </td>
我们可以使用强类型Html.HiddenFor helper方法,为模型属性ReturnUrl创建一个隐藏域,但是我们需要使用基于字符串的Html.Hidden helper为ProductID域。如果我们写成
1 @Html.HiddenFor(x => line.Product.ProductID)
helper会渲染一个
1 name="line.Product.ProductID" type="hidden" value="2"
的 field。field的name不能匹配CartController.RemoveFromCart action放的的参数名,它会防止默认的模型绑定工作,所以MVC框架不能调用这个方法。
1 public RedirectToRouteResult RemoveFromCart(Cart cart,int productId,string returnUrl) 2 3 <input id="ProductID" name="ProductID" type="hidden" value="1"/>4 <input id="ReturnUrl" name="ReturnUrl" type="hidden" value="/Watersports"/>
name与参数名相匹配。
5.2 添加购物车汇总
我们需要把购物车放在界面上。客户可以屏幕上看到购物车中,商品的数量。他们可以看到一个一个新商品进入购物车。
要做到这点,我们需要添加一个控件,汇总购车的contents,被点击后显示购物车的contents。这和导航控件很相似,做一个注入到Razor layout的action。
在CartController中添加
1 public ViewResult Summary(Cart cart) 2 { 3 return View(cart); 4 }
它仅需要渲染一个视图,提供当前Cart(从我们自定义的模型绑定中获得)作为视图数据。我们需要创建一个局部视图,它会在Summary方法被调用时,在response中被渲染。创建Summary的局部视图,强类型Cart。
1 @model SportsStore.Domain.Entities.Cart 2 3 @{ 4 Layout = null; 5 } 6 7 <div id="cart"> 8 <span class="caption"> 9 <b>Your cart:</b>10 @Model.Lines.Sum(x=>x.Quantity) item(s), 11 @Model.ComputeTotalValue().ToString("c") 12 </span>13 14 @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery },null) 15 </div>
在_Layout.cshtml文件中?:
1 <div id="header">2 @{Html.RenderAction("Summary", "Cart");} 3 <div class="title">SPORTS STORE</div>4 </div>
使用RenderAction,结合action方法,渲染输出到页面。这是个不错的技术,打碎了程序的功能,使之成为不同的,可以再度重用的块。
6 提交订单
现在,偶们到达了最后一个客户特性,结账的能力和完成订单。接下来,我们会扩展领域模型,支持从用户捕捉购物明细,并添加处理这些细节的特性。
6.1 扩展领域模型
在Entities中添加ShippingDetails类,这个类代表了用户的购物明细。
1 publicclass ShippingDetails 2 { 3 [Required(ErrorMessage ="Please enter a name")] 4 publicstring Name { get; set; } 5 6 [Required(ErrorMessage ="Please enter the first address line")] 7 publicstring Line1 { get; set; } 8 publicstring Line2 { get; set; } 9 publicstring Line3 { get; set; } 10 11 [Required(ErrorMessage ="Please enter a city name")] 12 publicstring State { get; set; } 13 14 publicstring Zip { get; set; } 15 16 [Required(ErrorMessage ="Please enter a country name")] 17 publicstring Country { get; set; } 18 19 publicbool GiftWrap { get; set; } 20 }
使用了System.ComponentModel.DataAnnotations的验证属性。必须添加引用才能使用。ShippingDetails类中没有任何函数,所以我们没有明显的单元测试。
6.2 添加结账处理
我们的目标是用户可以输入他们的购物详情,并提交订单。我们需要添加Checkout now按钮到Views/Cart/Index.cshtml文件。
1 <p align-"center"class="actionButtons">2 <a href="@Model.ReturnUrl">Continue shopping</a>3 @Html.ActionLink("Checkout now","Checkout") 4 </p>
这个按钮调用了Cart/Checkout,所以要在CartController类中添加Checkout方法。这个方法返回默认视图,并传递一个新的ShippingDetails对象,作为视图模型。创建强类型视图,视图模型为ShippingDetails。
1 @model SportsStore.Domain.Entities.ShippingDetails 2 3 @{ 4 ViewBag.Title ="SportsStroe: Checkout"; 5 } 6 7 <h2>Check out now</h2> 8 Please enter your details, and we'll ship your goods right away! 9 @using(Html.BeginForm()){ 10 <h3>Ship to</h3>11 <div>Name: @Html.EditorFor(x=>x.Name)</div>12 13 <h3>Address</h3>14 <div>Line 1: @Html.EditorFor(x=>x.Line1)</div>15 <div>Line 2: @Html.EditorFor(x=>x.Line2)</div>16 <div>Line 3: @Html.EditorFor(x=>x.Line3)</div>17 <div>City: @Html.EditorFor(x=>x.City)</div>18 <div>State: @Html.EditorFor(x=>x.State)</div>19 <div>Zip: @Html.EditorFor(x=>x.Zip)</div>20 <div>Country: @Html.EditorFor(x=>x.Country)</div>21 22 <h3>Options</h3>23 <label>24 @Html.EditorFor(x=>x.GiftWrap) 25 </label>26 27 <p align="center">28 <input class="actionButtons" type="submit" value="Complete order"/>29 </p>30 } 31 32
使用Html.EditorFor helper方法,为每个表单域渲染了input元素。这个方式是一个templated view helper。我们让MVC框架画出input元素类型的必须的视图模型属性,而不是明确地使用Html.TextBoxFor指定它。
我们看到模板视图助手,多么只能地为我们的bool属性,渲染了一个checkbox。为string属性渲染了textbox。
我们将来会使用Html.EditorForModel helper方法,它会为ShippingDetails视图模型类的所有属性生成一个label和一个inputs。然而,我们想将name,address区分开来,并且显示在表单的不同区域,所以简单地直接参照每个属性。
6.3 实现Order Processor
我们需要一个组件,提交订单给处理。为了保持MVC模型的原则,我们为这个功能定义一个接口,并写一个它的实现,关联到DI容器和Ninject。
6.3.1 定义接口
在Abstrack文件夹中创建新接口IOrderProcessor。
1 publicinterface IPrderProcessor 2 { 3 void ProcessOrder(Cart cart, ShippingDetails shippingDetails); 4 }
6.3.2 接口的实现
IOrderProcessor的实现,用来处理订单,发e-mail给管理员。当然,我们简化了销售过程。大多数电子贸易网站,不会简单地将order发e-mail,但是我们不提供处理信用卡或其他形式的支付的支持。我们只想关注MVC,所以经它发e-mail。
在Concrete文件夹中创建EmailOrderProcessor类。这个类使用了.NET框架内建的SMTP支持,来发送e-mail。
为了让事情变得简单,我们定义了EmailSettings类,EmailOrderProcessor的构造器方法需要这个类的实例,它包含.NET e-mail类需要的所有配置。
不要担心没有SMTP可用,如果设置了EmailSetting.WriteAsFile属性为true,e-mail messages会被直接写入FileLocation指定的文件。这个途径必须存在而且可以写入。文件会以.eml扩展。
6.4 注册实现
现在偶们有了IOrderProcessor接口的实现,意味着可以配置它。我们可以使用Ninject创建它的实例。在NinjectControllerFactory类中添加绑定。
1 privatevoid AddBindings() 2 { 3 ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>(); 4 5 EmailSettings emailSettings =new EmailSettings 6 { 7 WriteAsFile =bool.Parse(ConfigurationManager.AppSettings["Email.WriteAsFile"] ??"false") 8 }; 9 10 ninjectKernel.Bind<IOrderProcessor>(). 11 To<EmailOrderProcessor>().WithConstructorArgument("settings", emailSettings); 12 }
我们创建了一个EmailSettings对象,当IOrderProcessor接口被请求创建一个新的实例时,偶们使用Ninject WithConstructorArgument方法将它注入到EmailOrderProcessor的构造器中。我只指定了一个属性,WriteAsFile。它允许我们访问Web.config文件中的程序设置。
1 <appSettings>2 <add key="ClientValidationEnabled" value="true"/>3 <add key="UnobtrusiveJavaScriptEnabled" value="true"/>4 <add key="Email.WriteAsFile" value="true"/>5 </appSettings>
6.5 完成Cart Controller
要完成CartController类,我们需要修改构造函数,让它需要一个IOrderProcessor接口的实例,并添加一个新的action方法,处理当用户点击Complete Order按钮时的HTTP表单POST。
1 private IProductRepository repository; 2 private IOrderProcessor orderProcessor; 3 4 public CartController(IProductRepository repo,IOrderProcessor proc) 5 { 6 repository = repo; 7 orderProcessor = proc; 8 } 9 10 [HttpPost] 11 public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) 12 { 13 if(cart.Lines.Count()==0){ 14 ModelState.AddModelError("", "Sorry,your cart is empty!"); 15 } 16 17 if (ModelState.IsValid){ 18 orderProcessor.ProcessOrder(cart, shippingDetails); 19 cart.Clear(); 20 return View("Completed"); 21 }else22 { 23 return View(shippingDetails); 24 } 25 }
Checkout方法使用HttpPost属性装饰,这意味着它会通过POST查询的方式调用。当用户提交表单。再一次,你依赖模型绑定系统,包括ShippingDetails参数(它通过HTTP表单数组自动被创建)和Cart参数(它使用自定义绑定创建)。
这个改变需要我们变更CartController类的单元测试,传递Null为新的构造器参数。
MVC框架会检查ShippingDetails的date annotation属性的验证约束。任何违反的都会通过ModelState属性传递给action方法。我们可以通过检查ModelState.IsValid属性,看看这里有没有问题。注意,如果购物车为空,我们调用Modelstate.AddModelError方法,注册一个错误消息。
6.5.1 订单处理的单元测试
要使得CartController类的单元测试变得完整,需要测试Checkout的重写方法。
1 [TestMethod] 2 publicvoid Cannot_Checkout_Empty_Cart() 3 { 4 //Arrange - create a mock order processor 5 Mock<IOrderProcessor> mock =new Mock<IOrderProcessor>(); 6 //Arrange - create an empty cart 7 Cart cart =new Cart(); 8 //Arrange - create shipping details 9 ShippingDetails shippingDetails =new ShippingDetails(); 10 //Arrange - create an instance of the controller11 CartController target =new CartController(null, mock.Object); 12 13 //Act14 ViewResult result = target.Checkout(cart, shippingDetails); 15 16 //Assert - check that the order hasn't been passed on to the processor17 mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never()); 18 //Assert - check that the method is returning the default view19 Assert.AreEqual("", result.ViewName); 20 //Assert - check that we are passing an invalid model to the view21 Assert.AreEqual(false, result.ViewData.ModelState.IsValid); 22 } 23 24 [TestMethod] 25 publicvoid Cannot_Checkout_Invalid_ShippingDetails() 26 { 27 //Arrange - create a mock order processor28 Mock<IOrderProcessor> mock =new Mock<IOrderProcessor>(); 29 30 //Arrange - create a cart with an item31 Cart cart =new Cart(); 32 cart.AddItem(new Product(), 1); 33 34 //Arrange - create an instance of the controller35 CartController target =new CartController(null, mock.Object); 36 //Arrange - add an error to the model37 target.ModelState.AddModelError("error", "error"); 38 39 //Act - try to checkout40 ViewResult result = target.Checkout(cart, new ShippingDetails()); 41 42 //Assert - check that the order hasn't been passed on the processor43 mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never()); 44 //Assert - check that the method is returning the default view45 Assert.AreEqual("", result.ViewName); 46 //Assert - check that we are passing an invalid model to the view47 Assert.AreEqual(false, result.ViewData.ModelState.IsValid); 48 49 } 50 51 [TestMethod] 52 publicvoid Can_Checkout_And_Submit_Order() 53 { 54 //Arrange - create a mock order processor55 Mock<IOrderProcessor> mock =new Mock<IOrderProcessor>(); 56 //Arrange - create a cart with an item57 Cart cart =new Cart(); 58 cart.AddItem(new Product(), 1); 59 //Arrange - create an instance of the controller60 CartController target =new CartController(null, mock.Object); 61 62 //Act - try to checkout63 ViewResult result = target.Checkout(cart, new ShippingDetails()); 64 65 //Assert - check that the order has been passed on to the processor66 mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never()); 67 //Assert - check that the method is returning the Completed vie68 Assert.AreEqual("Completed", result.ViewName); 69 //Assert - check that we are passing a valid model to the view70 Assert.AreEqual(true, result.ViewData.ModelState.IsValid); 71 }
测试确保了不能check out使用空购物车。我们检查这点,通过确保mock IOrderProcessor实现的ProcessOrder永远不会被调用。model state被标记为invalid,传递给view。
6.6 显示验证错误
如果用户输入不能通过验证的信息,这个表单域就会高亮,但不显示错误信息。如果用户使用空的购物车结账,我们不让他完成订单,但是它看不到任何错误信息。为了解决这点,我们需要添加验证汇总到Checkout.cshtml视图。
1 Please enter your details, and we'll ship your goods right away! 2 @using (Html.BeginForm()) { 3 4 @Html.ValidationSummary() 5 6 <h3>Ship to</h3>
6.7 显示总结页面
我们要显示一个确认订单已经处理完毕,感谢他们购买。为Checkout方法添加Completed视图。
1 @{ 2 ViewBag.Title ="SportsStore: Order Submitted"; 3 } 4 5 <h2>Thanks!</h2>6 Thanks for placing your order. We'll ship your goods as soon as possible.7
7 总结
偶们有一个可以浏览分类和页面的产品分类。一个优雅的购物车,一个简单的结账过程。完好分离的建筑学,以为着偶们可以简单的改变程序任意一部分的功能,而不用担心产生问题或与其他地方不一致。例如,我们可以使用数据库存储订单,并且对它在购物车中,产品分类中,或程序的任何部分,都没有影响。