CHAPTER 9 SportsStore: Completing the Cart Trong chương này, tiếp tục xây dựng các SportsStore tương tự app. Trong chương trước, Chúng ta thêm hổ trợ căn bản cho việc mua cart và bây giờ chúng ta sẽ cải thiện và hoàn thành chức năng này.
Sử dụng Model Binding Các MVC Framework sử dụng một hệ thống gọi là model binding – mô hình ràng buộc để tạo ra đối tượng C # từ yêu cầu HTTP để chuyển chúng như các giá trị tham số cho các phương thức hoạt động. Đây là cách các framework MVC xử lý form, ví dụ: nhìn vào các thông số của các phương thức hoạt động được đạt ra và sử dụng một model binder để có các giá trị form gửi bởi trình duyệt và chuyển đổi chúng sang các kiểu của tham số cùng dạng trước khi chuyển chúng đến các phương thức hoạt động. Mô hình binders có thể tạo ra C # chuẩn từ bất kỳ thông tin có sẵn trong yêu cầu. Đây là một trong những đặc điểm chính của MVC Framework. Tôi sẽ tạo ra một mô hình binder tùy chỉnh để cải thiện class CartController. Tôi thích sử dụng các tính năng trong bộ điều khiển Cart để lưu trữ và quản lý các đối tượng Cart mà tôi thiết lập trong Chương 8, nhưng tôi không thích cách kiểm tra của nó. Nó không phù hợp với phần còn lại của các mô hình ứng dụng, mà là dựa trên các thông số hoạt động. Có thể có các đơn vị sai trong class CartController muốn kiểm tra tôi phải thử các tham số của lớp cơ sở. Để giải quyết vấn đề này, tôi sẽ tạo ra một mô hình Binder tùy chỉnh có thể lấy được các đối tượng Cart chứa trong data. Trong MVC Framework sẽ tạo ra các đối tượng Cart và làm chúng như tham số cho các phương pháp hoạt động trong lớp CartController. Các tính năng ràng buộc mô hình mạnh mẽ và linh hoạt. Đi sâu hơn về tính năng này trong Chương 24.
Creating a Custom Model Binder Tôi tạo một mô hình Binder tuỳ chỉnh bằng triển khai interface System.Web.Mvc.IModelBinder. Để thực hiện, thêm một thư mục vào project SportsStore.WebUI được gọi là Infrastructure/Binders và tạo một tập tin lớp
CartModelBinder.cs bên trong đó. Listing 9-1 cho thấy các nội dung của tập tin mới.
Listing 9-1. The Contents of the CartModelBinder.cs File using System.Web.Mvc; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Infrastructure.Binders { public class CartModelBinder : IModelBinder { private const string sessionKey = "Cart"; public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { // get the Cart from the session Cart cart = null; if (controllerContext.HttpContext.Session != null) { cart =(Cart)controllerContext.HttpContext.Session[sessionKey]; } // create the Cart if there wasn't one in the session data if (cart == null) { cart = new Cart(); if (controllerContext.HttpContext.Session != null) { controllerContext.HttpContext.Session[sessionKey] =cart; } } // return the cart return cart; } } }
Giao diện IModelBinder định nghĩa một phương thức: BindModel. Hai thông số được cung cấp để làm cho việc tạo ra các mô hình đối tượng miền. Các ControllerContext cung cấp quyền truy cập vào tất cả các thông tin mà các lớp điều khiển có, trong đó bao gồm chi tiết về các yêu cầu từ khách hàng. Các ModelBindingContext cung cấp cho bạn thông tin về các đối tượng mô hình bạn đang được yêu cầu xây dựng và một số công cụ để làm cho quá trình liên kết dễ dàng hơn. Đối với mục đích của tôi, lớp ControllerContext là thứ tôi đang quan tâm. Nó có tính năng HttpContext,mà lần lượt có một tính năng Session cho phép tôi có được và thiết lập dữ liệu session. Tôi có thể có được các đối tượng Cart liên kết với session người dùng bằng cách đọc một giá trị từ dữ liệu session, và tạo ra một Cart được nếu không có. Tôi cần phải nói cho MVCFramework rằng nó có thể sử dụng lớp CartModelBinder để tạo ra các trường của Cart. Tôi làm điều này trong các phương pháp Application_Start của Global.asax, như thể hiện trong Listing 9-2. Listing 9-2. Registering the CartModelBinder Class in the Global.asax.cs File using System; using System.Collections.Generic;
using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; using SportsStore.Domain.Entities; using SportsStore.WebUI.Infrastructure.Binders; namespace SportsStore.WebUI { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder());
} } }
Bây giờ tôi có thể cập nhật các bộ điều khiển Cart để loại bỏ các phương pháp getCart và dựa trên các mô hình Binder cung cấp bộ điều khiển với các đối tượng của Cart. Listing 9-3 cho thấy các thay đổi. Listing 9-3. Relying on the Model Binder in the CartController.cs File using System.Linq; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository; public CartController(IProductRepository repo) { repository = repo; } public ViewResult Index(Cart cart, string returnUrl) { return View(new CartIndexViewModel { ReturnUrl = returnUrl, Cart = cart }); } public RedirectToRouteResult AddToCart(Cart cart, int productId, string returnUrl) { Product product = repository.Products.FirstOrDefault(p => p.ProductID == productId); if (product != null) {
cart.AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); } public RedirectToRouteResult RemoveFromCart(Cart cart, int
Tôi đã gỡ bỏ các phương pháp getCart và thêm một tham số Cart để mỗi phương thức hoạt động. Khi các MVC Framework nhận được yêu cầu, các phương pháp AddToCart sẽ được gọi, nó sẽ bắt đầu bằng cách nhìn vào các thông số cho các phương pháp hoạt đông. Nó nhìn vào danh sách các Binders có sẵn và sẽ cố gắng tạo ra trường hợp của từng loại tham số. Các Binders tùy chỉnh được yêu cầu tạo một đối tượng Cart, và nó làm như vậy bằng cách làm việc với các tính năng trạng thái session. Giữa các Binder tùy chỉnh và các Binder mặc định, các MVC Framework có thể tạo ra các tập hợp các thông số cần thiết để gọi phương thức hoạt động, cho phép tôi cấu trúc lại bộ điều khiển để nó không hiểu sai về đối tượng Cart được tạo ra khi nhận được yêu cầu. Có rất nhiều lợi ích khi sử dụng Binder tùy chỉnh mô hình như thế này. Việc đầu tiên là tôi đã phân chia các logic được sử dụng để tạo ra một Cart từ bộ điều khiển, cho phép tôi thay đổi cách tôi lưu trữ đối tượng Cart mà không cần phải thay đổi bộ điều khiển. Lợi ích
thứ hai là bất kỳ class điều khiển mà làm việc với các đối tượng Cart chỉ có thể khai báo chúng như tham số phương thức hoạt động và tận dụng mô hình tùy chỉnh Binder. Lợi ích thứ ba quan trọng nhất, tôi bây giờ có thể kiểm tra đơn vị điều khiển Cart mà không cần phải thử rất nhiều ASP.NET plumbing. UNIT TEST: THE CART CONTROLLER Tôi có thể kiểm tra đơn vị class CartController bằng cách tạo ra các đối tượng Cart và chuyển chúng tới các phương pháp hoạt động. Tôi muốn thử nghiệm ba khía cạnh khác nhau của bộ điều khiển này: Phương pháp AddToCart nên thêm tìm kiếm product trong tuỳ chĩnh Cart. Sau khi thêm một product vào cart, người sử dụng nên được chuyển đến giao diện Index. URL mà người dùng có thể trở về danh mục cần được thông qua một cách chính xác để các phương thức hoạt động Index. Tôi thêm file CartTests.cs trong project portsStore.UnitTests: using using using using using using
using System.Web.Mvc; using SportsStore.WebUI.Models; namespace SportsStore.UnitTests { [TestClass] public class CartTests { // . . . existing test methods omitted for brevity. . . [TestMethod] public void Can_Add_To_Cart() { // 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", Category = "Apples"}, }.AsQueryable()); // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(mock.Object); // Act - add a product to the cart target.AddToCart(cart, 1, null); // Assert Assert.AreEqual(cart.Lines.Count(), 1); Assert.AreEqual(cart.Lines.ToArray()[0].Product.ProductID, 1); } [TestMethod] public void Adding_Product_To_Cart_Goes_To_Cart_Screen() { // 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", Category =
"Apples"}, }.AsQueryable()); // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(mock.Object); // Act - add a product to the cart
RedirectToRouteResult result = target.AddToCart(cart, 2, "myUrl"); // Assert Assert.AreEqual(result.RouteValues["action"], "Index"); Assert.AreEqual(result.RouteValues["returnUrl"], "myUrl"); } [TestMethod] public void Can_View_Cart_Contents() { // Arrange - create a Cart Cart cart = new Cart(); // Arrange - create the controller CartController target = new CartController(null); // Act - call the Index action method CartIndexViewModel result = (CartIndexViewModel)target.Index(cart, "myUrl").ViewData.Model; // Assert Assert.AreSame(result.Cart, cart); Assert.AreEqual(result.ReturnUrl, "myUrl"); } } }
Hoàn thành Cart Tôi đã giới thiệu các mô hình tùy chỉnh binder, giờ là lúc để hoàn thành các chức năng cart bằng cách thêm hai tính năng mới. Việc đầu tiên sẽ cho phép khách hàng để loại bỏ một mục từ Cart. Tính năng thứ hai sẽ hiển thị một bản tóm tắt của các Cart ở trên cùng của trang.
Removing Items from the Cart Tôi đã xác định được và thử nghiệm các phương thức hoạt động RemoveFromCart trong bộ điều khiển, do đó cho phép các khách hàng loại bỏ mặt hàng này nhưng vấn đề phương pháp này trong một lần xem, mà tôi sẽ làm bằng cách thêm một nút Remove trong mỗi hàng của tóm tắt cart. Những thay đổi để Views/Cart/Index.cshtml được hiển thị trong Listing 9-4. Listing 9-4. Introducing a Remove Button to the Index.cshtml File @model SportsStore.WebUI.Models.CartIndexViewModel @{ ViewBag.Title = "Sports Store: Your Cart"; }
Tôi đã thêm một cột mới cho mỗi hàng của bảng có chứa một form với phần tử Input. Tôi đã định dạng phần tử Input như là một nút với Bootstrap và thêm một phần tử style và một id vào phần tử table để đảm bảo rằng các nút và các nội dung của các cột khác được liên kết đúng. Lưu ý: Tôi dùng các phương thức trợ giúp Html.HiddenFor để tạo ra một field ẩn cho thuộc tính mô hình ReturnUrl, nhưng tôi đã phải sử dụng phương thức trợ giúp Html.Hidden kiểu chuỗi làm tương tự cho các field ProductID. Nếu tôi đã viết Html.HiddenFor(x => line.Product.ProductID), các helper tạo field ẩn với tên line.Product.ProductID. Tên của trường này sẽ không phù hợp với tên của các tham số cho phương thức hoạt động CartController.RemoveFromCart, mà sẽ ngăn chặn các mô hình Binders mặc định từ công việc, do đó các MVC Framework sẽ không thể gọi phương thức. Bạn có thể thấy các nút Remove tại nơi làm việc bằng cách chạy các ứng dụng và thêm các mục vào giỏ mua hàng. Hãy nhớ rằng các giỏ đã có chứa các chức năng để loại bỏ nó, mà bạn có thể kiểm tra bằng cách nhấn vào một trong các nút mới, như thể hiện trong Figure 9-1.
Figure 9-1. Xóa một món hàng từ giỏ hàng
Adding the Cart Summary
Tôi có thể có một chức năng cart, nhưng có một vấn đề với cách mà nó được tích hợp vào giao diện. Khách hàng có thể cho biết những gì có trong cart của họ chỉ bằng cách xem màn hình cart summary. Và họ có thể xem màn hình cart summary chỉ bằng cách thêm một mục mới vào cart. Để giải quyết vấn đề này, tôi sẽ thêm một tiện ích tóm tắt nội dung của cart và có thể bấm để hiển thị các nội dung cart thông qua ứng dụng. Tôi sẽ làm điều này như cách mà tôi đã thêm widget như chuyển hướng một hoạt động output tôi sẽ bố trí vào các Razor. Để bắt đầu, tôi cần phải thêm các phương thức đơn giản, thể hiện trong Listing 9-5, đến class CartController. Listing 9-5. Adding the Summary Method to the CartController.cs File using System.Linq; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository;
public CartController(IProductRepository repo) { repository = repo; } // . . . other action methods omitted for brevity . . . public PartialViewResult Summary(Cart cart) { return PartialView(cart); } } }
Phương thức đơn giản này cần phải trả về một giao diện, cung cấp Cart hiện tại (mà sẽ thu được bằng cách sử dụng mô hình binder tùy chỉnh) như dữ liệu giao diện. Để tạo giao diện, kích chuột phải vào các phương thức action Summary và chọn Add View từ menu pop-up. Đặt tên cho giao diện để Summary và kích vào nút OK để tạo tập tin Views/Cart/Summary.cshtml. Chỉnh sửa các khung giao diện sao cho nó trùng khớp Listing 9-6. Listing 9-6. The Contents of the Summary.cshtml File
@model SportsStore.Domain.Entities.Cart <div class="navbar-right"> @Html.ActionLink("Checkout", "Index", "Cart", new { returnUrl = Request.Url.PathAndQuery }, new { @class = "btn btn-default navbar-btn" }) </div> <div class="navbar-text navbar-right"> <b>Your cart:</b> @Model.Lines.Sum(x => x.Quantity) item(s), @Model.ComputeTotalValue().ToString("c") </div> Giao diện hiển thị số lượng mặt hàng trong giỏ hàng, tổng chi phí của những mặt hàng, và một liên kết hiển thị chi tiết của giỏ hàng cho người sử dụng. Bây giờ tôi đã tạo ra giao diện được trả về bởi phương thức action Summary, tôi có thể gọi phương thức action Summary trong tập tin _Layout.cshtml để hiển thị tóm tắt giỏ, như thể hiện trong Listing 9-7. Listing 9-7. Adding the Summary Partial View to the _Layout.cshtml File <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initialscale=1.0"> <link href="∼/Content/bootstrap.css" rel="stylesheet" />
Bạn có thể thấy bản tóm tắt giỏ hàng chạy bởi ứng dụng. Số lượng món hàng và tổng số tiền khi bạn thêm món hàng vào giỏ hàng, như Figure 9-2.
Figure 9-2. The cart summary widget Với sự bổ sung này, khách hàng biết những gì có trong giỏ hàng của họ một cách rõ ràng, kiểm tra từ cửa hàng. Bạn có thể thấy, một lần nữa sử dụng phương thức Html.Action giúp kết hợp các đầu ra từ một phương pháp hành động trong các view khác. Đây là kỹ thuật tốt để phá bỏ các chức năng của một ứng dụng vào riêng biệt, dãy sử dụng lại.
Submitting Orders Bây giờ đến tính năng cuối cùng trong SportsStore: kiểm tra và hoàn thành một đơn đặt hàng. Trong các phần sau, tôi sẽ mở rộng mô hình miền để cung cấp hỗ trợ chi tiết vận chuyển từ một người sử dụng và thêm hỗ trợ ứng dụng để xử lý các chi tiết đó.
Extending the Domain Model Thêm một tập tin class gọi là ShippingDetails.cs vào thư mục Entities của project SportsStore.Domain và chỉnh sửa nó cho phù hợp với nội dung được hiển thị trong Listing 9-8. Đây là lớp sẽ được dùng để đại diện cho các chi tiết giao hàng cho khách hàng. Listing 9-8. The Contents of the ShippingDetails.cs File using System.ComponentModel.DataAnnotations; namespace SportsStore.Domain.Entities { public class ShippingDetails { [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; } }
}
Bạn có thể thấy tôi đang sử dụng các thuộc tính xác nhận từ namespace System.ComponentModel.DataAnnotations, giống như tôi đã làm trong Chapter 2. Tôi sẽ phân tích rõ hơn ở Chapter 25. Lưu ý: Lớp ShippingDetails không có bất kỳ chức năng, vì vậy không có thể kiểm thử unit test.
Adding the Checkout Process Mục đích là để người dùng có thể nhập thông tin vận chuyển của họ và nộp đơn đặt hàng của họ. Tôi cần phải thêm một nút Checkout để xem tóm tắt giỏ hàng. Listing 9-9 cho thấy sự thay đổi tôi áp dụng cho tập tin Views/Cart/Index.cshtml. Listing 9-9. Adding the Checkout Now Button to the Index.cshtml File . . . <div class="text-center"> <a class="btn btn-primary" href="@Model.ReturnUrl">Continue shopping</a> @Html.ActionLink("Checkout now", "Checkout", null, new { @class
= "btn btn-primary"}) </div> . . .
Sự thay đổi này tạo ra một liên kết kiểu như một nút, và khi nhấn vào, gọi phương thức action Checkout của controller Cart. Bạn có thể thấy nút này sẽ xuất hiện trong Figure 9-3.
Figure 9-3. The Checkout now button Như bạn mong đợi, bây giờ tôi cần định nghĩa phương thức Checkout trong lớp CartController, như Listing 9-10.
Listing 9-10. The Checkout Action Method in the CartController.cs File
using System.Linq; using System.Web.Mvc; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository; public CartController(IProductRepository repo) { repository = repo; } // . . . other action methods omitted for brevity . . . public ViewResult Checkout() {
return View(new ShippingDetails()); } } }
Phương thức Checkout trả về giao diện mặc định và vượt qua một ShippingDetails mới đối tượng là các mô hình khung nhìn. Để tạo view cho các phương pháp hành động, nhấp chuột phải vào các phương thức action Checkout và chọn Add View từ menu pop-up. Đặt tên cho Checkout và nhấp vào nút OK. Visual Studio sẽ tạo tập tin Views/Cart/Checkout.cshtml, bạn nên điều chỉnh để phù hợp với Listing 9-11. Listing 9-11. The Contents of the Checkout.cshtml File @model SportsStore.Domain.Entities.ShippingDetails @{
ViewBag.Title = "SportStore: Checkout"; }
Check out now
Please enter your details, and we'll ship your goods right away!
<div class="checkbox"> <label> @Html.EditorFor(x => x.GiftWrap) Gift wrap these items </label> </div> <div class="text-center">
</div> }
Đối với mỗi thuộc tính trong các mô hình, tôi đã tạo ra một phần tử label và input được định dạng với Bootstrap để lấy thông tin người dùng nhập vào. Bạn có thể xem kết quả mà tôi đã tạo ra bằng cách bắt đầu ứng dụng và nhấn vào nút Checkout ở phía trên cùng của trang và sau đó nhấn Checkout, như thể hiện trong Figure 9-4. (Bạn cũng có thể đạt được giao diện này bằng cách điều hướng đến URL /Cart/Checkout).
Figure 9-4. The shipping details form Vấn đề với giao diện này là nó có chứa rất nhiều đánh dấu lặp đi lặp lại. Sử dụng MVC Framework giúp HTML để có thể làm giảm sự trùng lặp, nhưng chúng khó cấu trúc và định dạng nội dung theo cách mà tôi muốn. Thay vào đó, tôi sẽ sử dụng một tính năng lấy metadata về các đối tượng mô hình giao diện và kết hợp nó với một kết hợp của C# và biểu thức Razor. Bạn có thể thấy những gì tôi đã làm trong Listing 9-12.
Listing 9-12. Reducing Duplication in the Checkout.cshtml File @model SportsStore.Domain.Entities.ShippingDetails @{ ViewBag.Title = "SportStore: Checkout"; }
Check out now
Please enter your details, and we'll ship your goods right away!
foreach (var property in ViewData.ModelMetadata.Properties) { if (property.PropertyName != "Name" && property.PropertyName != "GiftWrap") { <div class="form-group"> <label>@(property.DisplayName ?? property.PropertyName) </label> @Html.TextBox(property.PropertyName, null, new {@class =
"form-control"}) </div> } }
Options
<div class="checkbox"> <label> @Html.EditorFor(x => x.GiftWrap) Gift wrap these items </label> </div> <div class="text-center"> /> </div> }
Thuộc tính static ViewData.ModelMetadata trả về một đối tượng System.Web.MVC.ModelMetaData cung cấp thông tin về các loại mô hình cho giao diện hiển thị. Thuộc tính Properties tôi sử dụng trong vòng lặp foreach trả về một tập hợp của các đối tượng ModelMetaData, mỗi đối tượng trong số đó đại diện cho một thuộc tính được xác định bởi loại mô hình. Tôi sử dụng property PropertyName để đảm bảo rằng tôi không tạo ra nội dung cho properties Name hoặc GiftWrap và tạo ra một tập hợp các thành phần, hoàn chỉnh với các lớp Bootstrap, cho tất cả các properties khác.
Tip: Từ khoá for và if mà tôi đã sử dụng nằm trong phạm vi của một biểu thức Razor (biểu thức @using tạo form), và vì vậy tôi không cần phải thêm tiền tố với ký tự @. Trong thực tế, tôi đã làm như vậy, Razor sẽ báo lỗi. Nó có thể mất một chút thời gian để có được sử dụng để khi các ký tự @ là cần thiết với Razor, nhưng nó trở thành bản chất thứ hai đối với hầu hết
các lập trình viên. Đối với những người không thể hoàn toàn nhận được nó ngay lần đầu tiên (trong đó bao gồm tôi), các thông báo lỗi Razor hiển thị trong trình duyệt cung cấp các hướng dẫn cụ thể để sửa chữa bất kỳ sai lầm. Tôi không thực hiện hoàn toàn được, tuy nhiên. Nếu bạn chạy ví dụ và nhìn vào đầu ra được tạo ra bởi các giao diện, bạn sẽ thấy rằng một số label đều không hoàn toàn chính xác, như Figure 9-5 minh họa.
Figure 9-5. The problem with generating labels from property names Kiểm tra xem nếu có một giá trị DisplayName có sẵn khi tôi tạo ra các thành phần của form, như thế này: . . . <label>@( property.DisplayName ?? property.PropertyName) </label> . . .
Để tận dụng lợi thế của property DisplayName, tôi cần phải áp dụng thuộc tính Display với lớp mô hình, như trong Listing 9-13. Listing 9-13. Áp dụng thuộc tính Display với tập tin ShippingDetails.cs using System.ComponentModel.DataAnnotations; namespace SportsStore.Domain.Entities { public class ShippingDetails { [Required( ErrorMessage = "Please enter a name" ) ] public string Name { get; set; } [Required( ErrorMessage = "Please enter the first address line" ) ] [Display(Name="Line 1" ) ] public string Line1 { get; set; } [Display(Name = "Line 2" ) ] public string Line2 { get; set; } [Display(Name = "Line 3" ) ] 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; } } }
Thiết lập giá trị Name cho thuộc tính Display cho phép tôi để thiết lập một giá trị sẽ được đọc bởi property DisplayName trong giao diện. Bạn có thể thấy hiệu ứng bằng cách khởi động ứng dụng và xem trang thanh toán, như thể hiện trong Figure 9-6.
Figure 9-6. The effect ofthe Display attribute on the model type Ví dụ này cho thấy hai khía cạnh khác nhau khi làm việc với MVC Framework. Việc đầu tiên là bạn có thể làm việc xung quanh tình trạng nào để đơn giản hóa việc đánh dấu hoặc viết mã của bạn. Thứ hai là mặc dù giao diện chạy trong mô hình MVC là hạn chế việc hiển thị dữ liệu và đánh dấu, các công cụ mà Razor và C# cung cấp cho nhiệm vụ này là phong phú và linh hoạt, thậm chí đến mức làm việc với các loại siêu dữ liệu.
Implementing the Order Processor Tôi cần một thành phần trong các ứng dụng để chi tiết của một đơn đặt hàng để xử lý. Để phù hợp với các nguyên tắc của mô hình MVC, tôi sẽ định nghĩa một interface cho chức năng này, viết một thực thi của interface, và sau đó kết hợp hai cái bằng container DI, Ninject.
Defining the Interface Thêm một interface mới gọi là IOrderProcessor vào thư mục Abstract của dự án
SportsStore.Domain và chỉnh sửa nội dung sao cho khớp Listing 9-14. Listing 9-14. The Contents of the IOrderProcessor.cs File
using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstract { public interface IOrderProcessor { void ProcessOrder( Cart cart, ShippingDetails shippingDetails) ; } }
Implementing the Interface Việc thực thi IOrderProcessor sẽ xử lý các đơn đặt hàng bằng cách gửi chúng vào trang quản trị. Tôi đơn giản hóa quá trình bán hàng, tất nhiên. Hầu hết các trang web thương mại điện tử sẽ không chỉ là một đơn đặt hàng e-mail đơn giản, và tôi đã không cung cấp hỗ trợ cho xử lý thẻ tín dụng hoặc các hình thức thanh toán khác. Nhưng tôi muốn giữ cho mọi thứ tập trung vào MVC, và như vậy sử dụng e-mail.
Tạo một tập tin lớp mới gọi là EmailOrderProcessor.cs trong thư mục bê tông của dự án SportsStore.Domain và chỉnh sửa nội dung sao cho khớp Listing 9-15. Lớp này sử dụng được xây dựng trong SMTP hỗ trợ bao gồm trong thư viện .NET Framework để gửi một e-mail. Listing 9-15. The Contents of the EmailOrderProcessor.cs File using System.Net; using System.Net.Mail; using System.Text; using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Concrete {
public class EmailSettings { public string MailToAddress = "" ; public string MailFromAddress = "" ; public bool UseSsl = true; public string Username = "MySmtpUsername" ; public string Password = "MySmtpPassword" ; public string ServerName = "smtp.example.com"; public int ServerPort = 587; public bool WriteAsFile = false; public string FileLocation = @"c:\sports_store_emails" ; } public class EmailOrderProcessor : IOrderProcessor { private EmailSettings emailSettings; public EmailOrderProcessor( EmailSettings settings) { emailSettings = settings; } public void ProcessOrder( Cart cart, ShippingDetails shippingInfo) {
using ( var smtpClient = new SmtpClient( ) ) { smtpClient.EnableSsl = emailSettings.UseSsl; smtpClient.Host = emailSettings.ServerName; smtpClient.Port = emailSettings.ServerPort; smtpClient.UseDefaultCredentials = false; smtpClient.Credentials = new NetworkCredential( emailSettings. Username, emailSettings.Password) ; if ( emailSettings.WriteAsFile) { smtpClient.DeliveryMethod =
SmtpDeliveryMethod.SpecifiedPickupDirectory; smtpClient.PickupDirectoryLocation = emailSettings.FileLocation; smtpClient.EnableSsl = false; } StringBuilder body = new StringBuilder( ) .AppendLine( " A new order has been submitted" ) .AppendLine( " ---" ) .AppendLine( " Items: " ) ; foreach ( var line in cart.Lines) { var subtotal = line.Product.Price * line.Quantity; body.AppendFormat( " { 0} x { 1} ( subtotal: { 2: c} " , line.Quantity, line.Product.Name, subtotal) ; } body.AppendFormat( " Total order value: { 0: c} " , cart.ComputeTotalValue( ) ) .AppendLine( " ---" ) .AppendLine( " Ship to: " ) .AppendLine( shippingInfo.Name) .AppendLine( shippingInfo.Line1) .AppendLine( shippingInfo.Line2 ?? "" ) .AppendLine( shippingInfo.Line3 ?? "" ) .AppendLine( shippingInfo.City) .AppendLine( shippingInfo.State ?? "" ) .AppendLine( shippingInfo.Country) .AppendLine( shippingInfo.Zip) .AppendLine( " ---" ) .AppendFormat( "Gift wrap: { 0} " , shippingInfo.GiftWrap ? "Yes" : "No" ) ;
MailMessage mailMessage = new MailMessage( emailSettings. MailFromAddress, //From emailSettings. MailToAddress, //To " New order submitted! ", //Subject body.ToString( ) ) ; //Body if ( emailSettings.WriteAsFile) { mailMessage.BodyEncoding = Encoding.ASCII; }
smtpClient.Send(mailMessage) ; } } } }
Để đơn giản, tôi đã định nghĩa lớp EmailSettings trong Listing 9-15. Một instance của lớp này được yêu cầu bởi cấu trúc EmailOrderProcessor và chứa tất cả các cài đặt cần thiết để cấu hình các lớp .NET e-mail. Tip: Đừng lo lắng nếu bạn không có một máy chủ SMTP có sẵn. Nếu bạn thiết lập property EmailSettings.WriteAsFile là true, các thông điệp e-mail sẽ được viết như tập tin vào thư mục chỉ định bởi property FileLocation. Thư mục này phải tồn tại và có khả năng ghi. Các tập tin sẽ được viết với mở rộng .eml, nhưng chúng có thể được đọc với bất kỳ trình soạn thảo văn bản. Vị trí tôi đã thiết lập trong listing là c:\sports_store_emails.
Registering the Implementation Bây giờ tôi có một thực thi interface IOrderProcessor và các phương tiện để cấu hình nó, tôi có thể sử dụng Ninject để tạo ra instance của nó. Chỉnh sửa tập tin NinjectDependencyResolver.cs trong thư mục Infrastructure của project SportsStore.WebUI
và thực hiện những thay đổi thể hiện trong Listing 9-16 với phương thức AddBindings. Listing 9-16. Adding Ninject Bindings for IOrderProcessor to the NinjectDependencyResolver.cs File using System; using System.Collections.Generic; using System.Configuration; using System.Web.Mvc; using Moq; using Ninject; using SportsStore.Domain.Abstract; using SportsStore.Domain.Concrete; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Infrastructure { public class NinjectDependencyResolver : IDependencyResolver { private IKernel kernel; public NinjectDependencyResolver( IKernel kernelParam) { kernel = kernelParam; AddBindings( ) ; } public object GetService( Type serviceType) { return kernel.TryGet( serviceType) ; }
public IEnumerable<object> GetServices( Type serviceType) { return kernel.GetAll( serviceType) ; } private void AddBindings( ) { kernel.Bind<IProductRepository>( ).To<EFProductRepository>( ) ; EmailSettings emailSettings = new EmailSettings {
WriteAsFile = bool.Parse(ConfigurationManager .AppSettings["Email. WriteAsFile"] ?? " false" ) }; kernel.Bind<IOrderProcessor>().To<EmailOrderProcessor>() . WithConstructorArgument("settings" , emailSettings) ; } } } Tôi tạo ra một đối tượng EmailSettings, mà tôi sử dụng với các phương pháp Ninject WithConstructorArgument để tôi có thể thêm nó vào constructor EmailOrderProcessor khi các trường hợp mới được tạo ra để yêu cầu dịch vụ cho giao diện IOrderProcessor. Trong Liệt kê 9-16, tôi đã chỉ định một giá trị để chỉ một trong các thuộc tính EmailSettings: WriteAsFile. Tôi đọc giá trị của tài sản này bằng cách sử dụng ConfigurationManager. Tài sản AppSettings, cung cấp truy cập để cài đặt ứng dụng được định nghĩa trong Web.config (các file trong thư mục dự án root), được thể hiện trong Liệt kê 9-17. Bảng liệt kê 9-17. Cài đặt ứng dụng trong các tập tin Web.config ... <appSettings> <add key=" webpages: Version" value=" 3. 0. 0. 0" /> <add key=" webpages: Enabled" value=" false" /> <add key=" ClientValidationEnabled" value=" true" /> <add key=" UnobtrusiveJavaScriptEnabled" value=" true" /> <add key=" Email.WriteAsFile" value=" true" /> </appSettings> ...
Hoàn thành Cart Controller Để hoàn thành lớp CartController, tôi cần chỉnh sửa kiến trúc để yêu cầu thực hiện giao diện IOrderProcessor và thêm một phương thức hành động mới mà sẽ xử lý yêu cầu POST biểu mẫu HTTP khi người dùng nhấp chuột vào nút Complete order. Listing 918 cho thấy cả hai thay đổi.
Listing 9-18. Hoàn thành việc điều khiển trong tập tin CartController.cs using System.Linq; using System.Web.Mvc;
using SportsStore.Domain.Abstract; using SportsStore.Domain.Entities; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.Controllers { public class CartController : Controller { private IProductRepository repository; private IOrderProcessor orderProcessor; public CartController(IProductRepository repo, IorderProcessor proc) { repository = repo; orderProcessor = proc; } // ... các phương thức action được bỏ qua cho ngắn gọn ... public ViewResult Checkout() { return View(new ShippingDetails()); } [HttpPost] public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) { if (cart.Lines.Count() == 0) { ModelState.AddModelError("", "Sorry, your cart is empty!") ; } if (ModelState.IsValid) { orderProcessor.ProcessOrder(cart, shippingDetails);
Bạn có thể thấy rằng phương thức Checkout tôi đã thêm được gắn với thuộc tính HttpPost, có nghĩa là nó sẽ được gọi cho một yêu cầu POST - trong trường hợp này, khi người dùng submit form. Một lần nữa, tôi dựa vào hệ thống nối kết mô hình, cho cả các tham số ShippingDetails (được tạo ra tự động bằng cách sử dụng các dữ liệu form HTTP) và các tham số Cart (được tạo ra bằng cách sử dụng kết nối tùy chỉnh). Chú ý: Sự thay đổi trong kiến trúc bắt buộc tôi cập nhật các kiểm thử tôi đã tạo cho lớp CartController. Gán null cho các tham số kiến trúc mới sẽ cho phép biên dịch các unit test. Các MVC Framework sẽ kiểm tra các ràng buộc validation mà tôi đã áp dụng để ShippingDetails sử dụng các thuộc tính chú thích dữ liệu, và bất kỳ vi phạm vấn đề validation được chuyền sang phương thức action thông qua thuộc tính ModelState. Tôi có
thể thấy nếu có bất kỳ vấn đề bằng cách kiểm tra thuộc tính ModelState.IsValid. Chú ý rằng tôi gọi phương thức ModelState.AddModelError để ghi một thông báo lỗi nếu không có sản phẩm nào trong giỏ hàng. Tôi sẽ giải thích làm thế nào để hiển thị lỗi như vậy trong thời gian ngắn, và tôi có nhiều hơn nữa để nói về mô hình liên kết - model binding và validation trong Chương 24 và 25. UNIT TEST: ORDER PROCESSING Để hoàn tất việc unit test cho lớp CartController, tôi cần phải kiểm thử cách hoạt động của phiên bản mới của phương thức Checkout.
Tôi muốn xử lý đơn hàng chỉ nếu có các món trong giỏ hàng và khách hàng đã cung cấp chi tiết giao hàng hợp lệ. Trong mọi trường hợp khác, các khách hàng sẽ được hiển thị một lỗi. Đây là phương thứ kiểm thử đầu tiên: . . . [TestMethod] public void Cannot_Checkout_Empty_Cart() { // Chuẩn bị – tạo một đơn hàng giả Mock<IOrderProcessor> mock = new Mock<IOrderProcessor>(); // Chuẩn bị – tạo một giỏ hàng trống Cart cart = new Cart(); // Chuẩn bị – tạo các chi tiết vận chuyển ShippingDetails shippingDetails = new ShippingDetails(); // Chuẩn bị – tạo một controller CartController target = new CartController(null, mock.Object); // Thực hiện ViewResult result = target.Checkout(cart, shippingDetails); // Xác nhận – kiểm tra đơn hàng không được thông qua vào bộ xử lý mock.Verify(m => m.ProcessOrder(It.IsAny<Cart>(), It.IsAny<ShippingDetails>()), Times.Never()); // Xác nhận – kiểm tra xem phương thức nào là trở về trang mặc định Assert.AreEqual("", result.ViewName); // Xác nhận - kiểm tra rằng tôi chuyển một mô hình hợp lệ sang giao diện hiển thị Assert.AreEqual(false, result.ViewData.ModelState.IsValid); } ...
Kiểm tra này đảm bảo rằng tôi không thể thanh toán với một giỏ rỗng. Tôi kiểm tra điều này bằng cách đảm bảo rằng ProcessOrder của việc thực hiện IOrderProcessor giả không bao giờ được gọi, trang mà phương thức trả về là trang mặc định (trong đó sẽ hiển thị
lại các dữ liệu đã nhập vào bởi khách hàng và cung cấp cho họ một cơ hội để sửa nó), và model state được chuyển tới trang đã được đánh dấu là không hợp lệ. Phương pháp kiểm thử tiếp theo hoạt động theo cách tương tự, nhưng thêm một lỗi vào mô hình view để mô phỏng một vấn đề bằng cách kết nối mô hình (điều đó sẽ xảy ra khi khách hàng nhập dữ liệu chuyển hàng không hợp lệ):