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
Trang 1CHAPTER 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
Trang 2Listing 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;
Đố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;
Trang 3public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
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
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index(Cart cart, string returnUrl) {
return View(new CartIndexViewModel {
Trang 4productId, string returnUrl) {
Product product = repository.Products.FirstOrDefault(p =>
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:
Trang 5public 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
Trang 6RedirectToRouteResult result = target.AddToCart(cart, 2,
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
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";
}
Trang 7<td>
@using (Html.BeginForm("RemoveFromCart", "Cart")) { @Html.Hidden("ProductId",line.Product.ProductID) @Html.HiddenFor(x => x.ReturnUrl)
<input class="btn btn-sm btn-warning" type="submit" value="Remove" />
Trang 8Tô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
Trang 9Figure 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
public class CartController : Controller {
private IProductRepository repository;
Trang 10public CartController(IProductRepository repo) {
repository = repo;
}
// other action methods omitted for brevity
public PartialViewResult Summary(Cart cart) {
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" })
Listing 9-7 Adding the Summary Partial View to the _Layout.cshtml File
<link href="∼/Content/bootstrap.css" rel="stylesheet" />
<link href="∼/Content/bootstrap-theme.css" rel="stylesheet" /> <title>@ViewBag.Title</title>
Trang 11</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">
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 đó
Trang 12Extending 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
Trang 13Figure 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
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
// other action methods omitted for brevity
public ViewResult Checkout() {
Trang 14return View(new ShippingDetails());
Listing 9-11 The Contents of the Checkout.cshtml File
@model SportsStore.Domain.Entities.ShippingDetails
@{
ViewBag.Title = "SportStore: Checkout";
}
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>
Trang 16Figure 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
Trang 17Listing 9-12 Reducing Duplication in the Checkout.cshtml File
@model SportsStore.Domain.Entities.ShippingDetails
@{
ViewBag.Title = "SportStore: Checkout";
}
<h2>Check out now</h2>
<p>Please enter your details, and we'll ship your goods right away!</p>
foreach (var property in ViewData.ModelMetadata.Properties) {
if (property.PropertyName != "Name" && property.PropertyName !=