【Pro ASP.NET MVC 3 Framework】.学习笔记.7.SportsStore:购物车

3 创建购物车

每个商品旁边都要显示Add to cart按钮。点击按钮后,会显示客户已经选中的商品的摘要,包括总金额。在购物车里,用户可以点击继续购物按钮返回product目录。也可以点击Checkout now按钮,完成订单和购物会话。

3.1 定义Cart Entity

购物车是程序业务域的一部分,在我们的领域模型中创建实体。添加一个Cart类到Entities文件夹。

1 namespace SportsStore.Domain.Entities 2 { 3 public class Cart 4 { 5 private List < CartLine > lineCollection = new List < CartLine > (); 6 7 public void 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 else 19 { 20 line.Quantity += quantity; 21 } 22 } 23 24 public void RemoveLine(Product product) 25 { 26 lineCollection.RemoveAll(l => l.Product.ProductID == product.ProductID); 27 } 28 29 public decimal ComputeTotalValue() 30 { 31 return lineCollection.Sum(e => e.Product.Price * e.Quantity); 32 } 33 34 public void Clear() 35 { 36 lineCollection.Clear(); 37 } 38 39 public IEnumerable < CartLine > Lines 40 { 41 get { return lineCollection; } 42 } 43 } 44 public class CartLine 45 { 46 public Product Product { get ; set ; } 47 public int Quantity { get ; set ; } 48 } 49 } 50

购物车类使用CartLine,代表用户选中的一个商品。定义了添加、移除、计算合计、清空的方法。我们也提供了一个属性,返回IEnumerble<CartLine。

3.1.1 测试购物车单元测试

Cart类相对简单,但有一些非常重要的行为,我们必须确保工作正常。一个功能不良的购物车会破坏程序的整体。偶们对这些特性一个一个测试。

第一个要测试的行为,是将添加货物到购物车。如果该商品是第一次被加到购物车,我们需要一个新的CartLine。

1 [TestMethod] 2 public void 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 // Act 12 target.AddItem(p1, 1 ); 13 target.AddItem(p2, 1 ); 14 CartLine[] results = target.Lines.ToArray(); 15 16 // Assert 17 Assert.AreEqual(results.Length, 2 ); 18 Assert.AreEqual(results[ 0 ].Product, p1); 19 Assert.AreEqual(results[ 1 ].Product, p2); 20 }

如果客户已经添加过该商品,我们需要增加相应CartLine的数量,而不是创建一个新的。

1 [TestMethod] 2 public void 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 // Act 12 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 // Assert 18 Assert.AreEqual(results.Length, 2 ); 19 Assert.AreEqual(results[ 0 ].Quantity, 11 ); 20 Assert.AreEqual(results[ 1 ].Quantity, 1 ); 21 }

我们也需要检查移除商品的功能。

1 [TestMethod] 2 public void 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 cart 10 Cart target = new Cart(); 11 12 // Arrange - add some products to the cart 13 target.AddItem(p1, 1 ); 14 target.AddItem(p2, 3 ); 15 target.AddItem(p3, 5 ); 16 target.AddItem(p2, 1 ); 17 18 // Act 19 target.RemoveLine(p2); 20 21 // Assert 22 Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0 ); 23 Assert.AreEqual(target.Lines.Count(), 2 ); 24 }

计算总金额的功能:

1 [TestMethod] 2 public void 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 // Act 12 target.AddItem(p1, 1 ); 13 target.AddItem(p2, 1 ); 14 target.AddItem(p1, 3 ); 15 decimal result = target.ComputeTotalValue(); 16 17 // Assert 18 Assert.AreEqual(result, 450M); 19 }

最后测试的是清空功能

1 [TestMethod] 2 public void 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 // Act 12 target.AddItem(p1, 1 ); 13 target.AddItem(p2, 1 ); 14 15 target.Clear(); 16 17 // Assert 18 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 public class 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的value 2 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 public class CartIndexViewModel 2 { 3 public Cart Cart { get ; set ; } 4 public string 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 public class CartModelBinder:IModelBinder 2 { 3 private const string sessionKey = " Cart " ; 4 5 public object 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 data 10 if (cart == null ){ 11 cart = new Cart(); 12 controllerContext.HttpContext.Session[sessionKey] = cart; 13 } 14 // return the cart 15 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 public void 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 Cart 13 Cart cart = new Cart(); 14 15 // Arragne - create the controller 16 CartController target = new CartController(mock.Object); 17 18 // Act - add a product to the cart 19 target.AddToCart(cart, 1 , null ); 20 21 // Assert 22 Assert.AreEqual(cart.Lines.Count(), 1 ); 23 Assert.AreEqual(cart.Lines.ToArray()[ 0 ].Product.ProductID, 1 ); 24 } 25 26 [TestMethod] 27 public void Adding_Product_To_Cart_Goes_To_Cart_Screen() 28 { 29 // Arrange - create the mock repository 30 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 Cart 38 Cart cart = new Cart(); 39 40 // Arragne - create the controller 41 CartController target = new CartController(mock.Object); 42 43 // Act - add a product to the cart 44 RedirectToRouteResult result = target.AddToCart(cart, 2 , " myUrl " ); 45 46 // Assert 47 Assert.AreEqual(result.RouteValues[ " action " ], " Index " ); 48 Assert.AreEqual(result.RouteValues[ " returnUrl " ], " myUrl " ); 49 } 50 51 [TestMethod] 52 public void Can_View_Cart_Contents() 53 { 54 // Arrange - create a Cart 55 Cart cart = new Cart(); 56 57 // Arragne - create the controller 58 CartController target = new CartController( null ); 59 60 // Act - call the Index action method 61 CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, " myUrl " ).ViewData.Model; 62 63 // Assert 64 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 public class ShippingDetails 2 { 3 [Required(ErrorMessage = " Please enter a name " )] 4 public string Name { get ; set ; } 5 6 [Required(ErrorMessage = " Please enter the first address line " )] 7 public string Line1 { get ; set ; } 8 public string Line2 { get ; set ; } 9 public string Line3 { get ; set ; } 10 11 [Required(ErrorMessage = " Please enter a city name " )] 12 public string State { get ; set ; } 13 14 public string Zip { get ; set ; } 15 16 [Required(ErrorMessage = " Please enter a country name " )] 17 public string Country { get ; set ; } 18 19 public bool 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 public interface 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 private void 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 } else 22 { 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 public void 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 controller 11 CartController target = new CartController( null , mock.Object); 12 13 // Act 14 ViewResult result = target.Checkout(cart, shippingDetails); 15 16 // Assert - check that the order hasn't been passed on to the processor 17 mock.Verify(m => m.ProcessOrder(It.IsAny < Cart > (), It.IsAny < ShippingDetails > ()), Times.Never()); 18 // Assert - check that the method is returning the default view 19 Assert.AreEqual( "" , result.ViewName); 20 // Assert - check that we are passing an invalid model to the view 21 Assert.AreEqual( false , result.ViewData.ModelState.IsValid); 22 } 23 24 [TestMethod] 25 public void Cannot_Checkout_Invalid_ShippingDetails() 26 { 27 // Arrange - create a mock order processor 28 Mock < IOrderProcessor > mock = new Mock < IOrderProcessor > (); 29 30 // Arrange - create a cart with an item 31 Cart cart = new Cart(); 32 cart.AddItem( new Product(), 1 ); 33 34 // Arrange - create an instance of the controller 35 CartController target = new CartController( null , mock.Object); 36 // Arrange - add an error to the model 37 target.ModelState.AddModelError( " error " , " error " ); 38 39 // Act - try to checkout 40 ViewResult result = target.Checkout(cart, new ShippingDetails()); 41 42 // Assert - check that the order hasn't been passed on the processor 43 mock.Verify(m => m.ProcessOrder(It.IsAny < Cart > (), It.IsAny < ShippingDetails > ()), Times.Never()); 44 // Assert - check that the method is returning the default view 45 Assert.AreEqual( "" , result.ViewName); 46 // Assert - check that we are passing an invalid model to the view 47 Assert.AreEqual( false , result.ViewData.ModelState.IsValid); 48 49 } 50 51 [TestMethod] 52 public void Can_Checkout_And_Submit_Order() 53 { 54 // Arrange - create a mock order processor 55 Mock < IOrderProcessor > mock = new Mock < IOrderProcessor > (); 56 // Arrange - create a cart with an item 57 Cart cart = new Cart(); 58 cart.AddItem( new Product(), 1 ); 59 // Arrange - create an instance of the controller 60 CartController target = new CartController( null , mock.Object); 61 62 // Act - try to checkout 63 ViewResult result = target.Checkout(cart, new ShippingDetails()); 64 65 // Assert - check that the order has been passed on to the processor 66 mock.Verify(m => m.ProcessOrder(It.IsAny < Cart > (), It.IsAny < ShippingDetails > ()), Times.Never()); 67 // Assert - check that the method is returning the Completed vie 68 Assert.AreEqual( " Completed " , result.ViewName); 69 // Assert - check that we are passing a valid model to the view 70 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 总结

偶们有一个可以浏览分类和页面的产品分类。一个优雅的购物车,一个简单的结账过程。完好分离的建筑学,以为着偶们可以简单的改变程序任意一部分的功能,而不用担心产生问题或与其他地方不一致。例如,我们可以使用数据库存储订单,并且对它在购物车中,产品分类中,或程序的任何部分,都没有影响。

你可能感兴趣的:(framework)