Tải bản đầy đủ (.pdf) (30 trang)

9 chuong 09 tủ tài liệu bách khoa

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (1.04 MB, 30 trang )

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


productId, string returnUrl) {
Product product = repository.Products.FirstOrDefault(p =>
p.ProductID == productId);
if (product != null) {
cart.RemoveLine(product);
}
return RedirectToAction("Index", new { returnUrl });
}
}
}

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

System;
Microsoft.VisualStudio.TestTools.UnitTesting;
SportsStore.Domain.Entities;
System.Linq;
Moq;
SportsStore.Domain.Abstract;


using SportsStore.WebUI.Controllers;

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";
}


<style>
#cartTable td { vertical-align: middle; }
</style>

Your cart


<table id="cartTable" class="table">
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
<th class="text-right">Price</th>
<th class="text-right">Subtotal</th>

</tr>
</thead>
<tbody>
@foreach (var line in Model.Cart.Lines) {
<tr>
<td class="text-center">@line.Quantity</td>
<td class="text-left">@line.Product.Name</td>
<td class="text-right">@line.Product.Price.ToString("c")
</td>
<td class="text-right">
@((line.Quantity * line.Product.Price).ToString("c"))
</td>
<td>
@using (Html.BeginForm("RemoveFromCart", "Cart")) {
@Html.Hidden("ProductId",line.Product.ProductID)
@Html.HiddenFor(x => x.ReturnUrl)
value="Remove" />
}
</td>
</tr>
}
</tbody>


<tfoot>
<tr>
<td colspan="3" class="text-right">Total:</td>
<td class="text-right">
@Model.Cart.ComputeTotalValue().ToString("c")

</td>
</tr>
</tfoot>
</table>
<div class="text-center">
<a class="btn btn-primary" href="@Model.ReturnUrl">Continue
shopping</a>
</div>

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" />

<link href="∼/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>


</head>
<body>
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">SPORTS STORE</a>
@Html.Action("Summary", "Cart")
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
@Html.Action("Menu", "Nav")
</div>
<div class="col-xs-8">
@RenderBody()
</div>
</div>
</body>
</html>

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!


@using (Html.BeginForm()) {

Ship to


<div class="form-group">
<label>Name:</label>
@Html.TextBoxFor(x => x.Name, new {@class = "form-control"})
</div>

Address


<div class="form-group">
<label>Line 1:</label>
@Html.TextBoxFor(x => x.Line1, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Line 2:</label>
@Html.TextBoxFor(x => x.Line2, new {@class = "form-control"})
</div>
<div class="form-group">
<label>Line 3:</label>
@Html.TextBoxFor(x => x.Line3, new {@class = "form-control"})
</div>
<div class="form-group">
<label>City:</label>
@Html.TextBoxFor(x => x.City, new {@class = "form-control"})
</div>
<div class="form-group">
<label>State:</label>
@Html.TextBoxFor(x => x.State, new {@class = "form-control"})

</div>
<div class="form-group">
<label>Zip:</label>
@Html.TextBoxFor(x => x.Zip, new {@class = "form-control"})


</div>
<div class="form-group">
<label>Country:</label>
@Html.TextBoxFor(x => x.Country, 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>
}

Đố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!


@using (Html.BeginForm()) {

Ship to


<div class="form-group">
<label>Name</label>
@Html.TextBoxFor(x => x.Name, new {@class = "form-control"})
</div>

Address


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);

cart.Clear();
return View("Completed");
} else {
return View(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ệ):


×