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

8 chuong 08 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.42 MB, 31 trang )

Chương 8 :
Cửa hàng thể thao trực tuyến : điều hướng
Điều khiển điều hướng:
Các ứng dụng của cửa hàng thể thao trực tuyến sẽ hữu dụng hơn khi
khách hàng có thể lựa chọn sản phẩm dựa theo từng mục. Ở đây sẽ có ba
giai đoạn:
.Nâng

cao

hoạt

động

của

model

List trong class

ProductControllerđể có thể chọn lọc các dụng cụ trong kho hàng.
.Sử dụng lại và nâng cao các đề án về URL và rà soát lại các
chiến lược định tuyến.
.Tạo một danh mục mà sẽ được đưa vào trong thanh sidebar
của site, làm nổi bật các danh mục và lien kết chúng với những thứ khác.

Lọc theo danh mục sản phẩm:
Nâng cao view model class ở class ProcductsListViewModel, là
class sẽ được thêm vào project SportsStore.WebUIở chương trước. Cần có
sự giao tiếp ( kết nối) tới view cũng như trả về cho sidebar. Listing 8-1 sẽ
thể hiện những thay đổi trong file ProductListView.cs .


Listing 8-1: Nâng cấp file Productview.cs
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models {
public class ProductsListViewModel {
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; }
public string CurrentCategory { get; set; }
}
}


CurrentCategorylà một tham số mới được thêm vào. Bước kế tiếp là sẽ
nâng cấp Product controllers để phương thức hoạt động củaListsẽ lọc
trong Product ra những sản phẩm theo danh mục và sử dụng tham số mới
để xem những mẫu mà đã được chọn lựa trong danh mục. Điều này sẽ được
thể hiện torng Listing 8-2.
Listing 8-2:thêm Category Support vào phương thức hoạt động của List
trong file ProcductController.cs .
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;

public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(string category , int page = 1) {
ProductsListViewModel model = new ProductsListViewModel {
Products = repository.Products
.Where(p => category == null || p.Category ==category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
},
CurrentCategory = category
};
return View(model);
}
}
}


Đã có 3 sự thay đổi đến phương thức hoạt động. Đầu tiên, thông số
category đã được thêm vào. Thông số category được sử dụng như là sự
thay đổi thứ hai trong Listing, là thông số được nâng cấp đến truy vấn LINQ.
Nếu mà Category không có giá trị null, thì chỉ có đối tượng Product khớp
với categoryđã chọn. Thay đổi cuối cùng là đặt lại giá trị của tham số
CurrentCategory đã được thêm vào class ProductListViewModel.cs,

Tuy nhiên, những thay đổi này sẽ làm tính toán giá trị của
PagInfo.TotalItems sai lệch.
UNIT TEST: UPDATING EXISTING UNIT TEST
Thay đổi các tín hiệu của phương thức hoạt động của List, mà có thể
ngăn chặn một số phương thức hiện hữu của unit test từ biên dịch.Để thực
hiện, cần phải đặt Null như là tham số đầu tiên của phương thức List trong
unit test mà đơn vị đó sẽ làm việc với controllers. Ví dụ, trong kiểm tra
Can_Paginate, section hoat động của unit test trở thành:
...
[TestMethod]
public void Can_Send_Pagination_View_Model() {
// Arrange
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
// Arrange
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// Act
ProductsListViewModel result
= (ProductsListViewModel)controller.List(null, 2).Model;

// Assert
PagingInfo pageInfo = result.PagingInfo;
Assert.AreEqual(pageInfo.CurrentPage, 2);

Assert.AreEqual(pageInfo.ItemsPerPage, 3);
Assert.AreEqual(pageInfo.TotalItems, 5);
Assert.AreEqual(pageInfo.TotalPages, 2);
}
...

Giữ cho việc đồng bộ giữ unit test với những thay đổi nhanh chóng
của code trở thành bản chất thứ hai khi bạn bắt đầu thử nghiệm mind-set.


Những ảnh hưởng của việc lọc danh mục là hiển nhiên, dù cho đó là
một thay đổi nhỏ. Khởi động chương trình và chọn lựa danh mục dựa vào
những chuỗi truy vấn, thay đổi port để chúng có thể tương thích với dự án
trên visual của bạn:
http://localhost:51280/?category=Soccer
Bạn sẽ chỉ nhận được sản phẩm có trong danh mục Soccer, Figure 81:

Chăc chắn, người dùng không muốn điều hướng tới danh mục bằng
URL, nhưng bạn có thề thấy một thay đổi nhỏ có ảnh hưởng lớn trong
chương trình MVC framework một khi cấu trúc cơ bản được đặt ra.


UNIT TEST: CATEGORY FILTERING
Cần có một unit test có thể kiểm tra đúng chức năng lọc danh sách,
để chắc chắn rằng bộ lộc có thể lọc chính xác bất cứ sản phẩm trong bất kì
danh mục nào :
[TestMethod]
public void Can_Filter_Products() {
// 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 = "Cat1"},
new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
});
// Arrange - create a controller and make the page size 3 items
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// Action
Product[] result = ((ProductsListViewModel)controller.List("Cat2",1).Model)
.Products.ToArray();
// Assert
Assert.AreEqual(result.Length, 2);
Assert.IsTrue(result[0].Name == "P2" && result[0].Category =="Cat2");
Assert.IsTrue(result[1].Name == "P4" && result[1].Category =="Cat2");
}
...

Việc test này tạo ra một kho lưu trữ mô hình các đối tượng Product
mà nó thuộc về một loạt các danh mục. Khi một danh mục cụ thể được yêu
cầu sử dụng dung phương thức Action, và kết quả được kiểm tra để chắc
chắn rằng đó là kết quả đúng với đối tượng sản phẩm đã đươc yêu cầu.

Refining the URL Scheme
Không một người dùng nào muốn thấy hay sử dụng một URL xấu
như là /?categiry=Soccer.Để thay đổi, cần xem lại hướng định tuyến để
tạo ra một cách tiếp cận khác tốt hơn cho người quản trị cũng như khách

hàng đến URL. Để thực hiện đổi mới đề án, cần thay đổi phương thức của
RegisterRoutes trong file App_Start/RouteConfig.cs.
Listing 8-3: làm mới URL Scheme trong file RouteConfig.cs


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace SportsStore.WebUI {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null,"",
new {
controller = "Product", action = "List",
category = (string)null, page = 1
}
);
routes.MapRoute(null,"Page{page}",
new { controller = "Product", action = "List", category
=(string)null },
new { page = @"\d+" }
);
routes.MapRoute(null,"{category}",
new { controller = "Product", action = "List", page = 1 }
);
routes.MapRoute(null,"{category}/Page{page}",

new { controller = "Product", action = "List" },
new { page = @"\d+" }
);
routes.MapRoute(null, "{controller}/{action}");
}
}
}

Lưu ý : việc thêm vào một định tuyến mới ở Listing 8-3 cho ta thấy là
rất quan trọng. Định tuyến được áp dụng trong thứ mà chúng được định
nghĩa, và bạn sẽ thấy được những sự thay đổi khi bạn đổi thứ tự.
Table 8-1 mô tả những những định tuyến đại diện cho URL Scheme.
Table 8-1 : Route Summary
URL
Lead To
/
Trang sản phẩm đầu tiên trong các mục
/Page 2
Các trang chỉ định khác, thể hiện các sản phẩm trong các
danh mục
Soccer
Thể hiện trang đầu tiên của các mục chỉ định
/Soccer/Page 2 Thể hiện các trang chỉ định của sản phẩm trong một danh


mục nhất định
Định tuyến của ASP.NET thì dựa trên nền tảng MVC để giải quyết
những yêu cầu imcoming từ client, cũng như tạo ra các outcoming URL
để có thể phù hợp với URL Scheme và có thể nhúng vào Web page. Bằng
việc sử dụng hệ thống định tuyến để giải quyết nhưng yêu cầu đến và tạo ra

những outgoing URLs, thì chắc chắn rằng tất cả các URLs trong chương
trình điều thích hợp.
Phương thức Url.Action là phương thức thuận tiện nhất để tạo
những liên kết đi. Ở chương trước, tác giả đã sử dụng cách thức hữu dụng
này trong phương thức List để xem các hiển thị trong trang liên kết. Tác giả
đã thêm những trợ giúp cho việc lọc danh mục, tác giả cần phải quay lại và
đặt thông tin cho phương thức giúp đỡ này.
Listing 8-4 :Thêm vào thông tin danh mục cho Pagination Link trong file
List.cshtml
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Products";
}
@foreach (var p in Model.Products) {
@Html.Partial("ProductSummary", p)
}
<div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List",
new { page = x, category = Model.CurrentCategory }))
</div>

Qua sự thay đổi này, đường dẫn được tạo ra cho các liên kết
pagination có dạng:

http://<myserver>:/Page1
Nếu người dùng chọn vào đường dẫn của trang như trên, thì lọc danh
mục mà người ấy đăng kí sẽ bị mất, và thay vào đó sẽ được thể hiện bằng
trang chứa các sản phẩm từ mọi danh mục. bằng việc thêm danh mục hiện
tại, lấy từ các mô hình điểm, tác giả đã tạo ra URL như sau:
http:// <myserver> : /Chess/Page1

Khi người dùng chọn vào những link kiểu như trên, thì danh mục hiện
tại sẽ chuyển tới hoạt động của phương thức List, và bộ lọc sẽ đươc bảo


quản. Sau bạn đã thực hiện những bước thay đổi này, bạn có thể vào các
URL như /Chess or /Soccer, và bạn có thể thấy chính xác liên kết trang ở
dưới cùng của trang cũng như danh mục sản phẩm.

Xây dựng danh mục điều hướng thể loại
Cần cung cấp cho người dùng một hướng để chọn danh mục mà
không cần phải nhập liệu vào URLs. Điều này có nghĩa là thể hiện chúng với
các danh sách của danh mục tồn tại và được chỉ định, nếu có, đang được
chọn. Như việc xây dựng chương trình, tác giả sử dụng danh sách các danh
mục cho nhiều hơn một controller, nên cần có gì đó có tính khép kín và có
thể tái sử dụng.
ASP.NET MVC Framework có một khái niệm là Child action, khái niệm
này thì tuyệt vời khi tạo một sản phẩm dựa trên việc tái sử dụng việc điều
khiển điều hướng. Child action dựa trên các phương pháp trợ giúp HTML
được gọi là Html.Action,phương thức cho phép bạn bao gồm các đầu ra từ
một phương pháp hành động tuỳ tiện nào trong giao diện hiện tại. Trong
trường hợp này, có thể tạo một điều khiển mới ( có thể gọi là
NavController) với phương thức hoạt động ( được gọi là Menu) hướng
tới một menu điều hướng. Html.Action sẽ được sử dụng để đưa output từ
phương thức vào layout.
Các tiếp cận này giúp mang lại một bộ điều khiển thực sự mà có thề
chứa bất cứ ứng dụng logic đươc sử dụng và có thể là unit test như những
điều khiển khác. Đây là một hướng tốt để tạo ra các segment nhỏ hơn của
ứng dụng trong khi vẫn bảo quản được cách tiếp cận MVC tổng thể.

Tạo một điều khiển điều hướng

Nhấn chuột phải vào thư mục Controller trong đề án
Sportstore.WebUI và chọn Add->Controller từ pop-up trong menu. Chọn
thẻ MVC 5 Controller – Empty, sau đó chọn Add, đặt tên cho controller là
NavController và chọn Add để tạo lớp NavController.cs. Xoá bỏ phương
thức Index để cho Visual Studio thêm vào một mặc định điều khiển mới và
phương thức hoạt động của Menu.
Listing 8-5: Thêm menu phương thức hoạt động vào NavController.cs
using System.Web.Mvc;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {


public string Menu() {
return "Hello from NavController";
}
}
}

Phương thức trả về một chuỗi thông báo tĩnh nhưng bấy nhiêu là đủ
để bắt đầu trong khi tích hợp các Child action còn lại của chương trình. Tác
giả muốn danh sách các thể loại xuất hiện tất cả trên một trang, vì vậy tác
giả trả lại các Child action vào layout, hơn là có một cái nhìn cụ thể. Điều
chỉnh View/Shared/_Layout.cshtml để có thể gọi phương thức trợ giúp
từ Html.Action.
Listing 8-6: Thêm hoạt động trả về vào file _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=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>
</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>

Ở đây đã xoá những văn bản giữ chỗ và thay thế bằng lệnh gọi
phương thức Html.Action. Những thông số trong phương thức này là tên
của những phương thức hoạt động mà tác giả muốn gọi (Menu) và những
điều khiển thuộc về nó (Nav). Nếu bạn chạy ứng dụng, bạn sẽ thấy ngõ ra
của phương thức Menu bao gồm cả những phản hồi đươc gởi tới trình
duyệt.


Figure 8-2:

Khởi tạo danh sách thể loại

Bây giờ tác giả có thể trả về giá trị cho controller Nav và khởi tạo một
giá trị thể loại thật.Tác giả không muốn khởi tạo URLs của thể loại trong
controller.Thay vào đó, tác giả sẽ sữ dụng phương thức hỗ trợ trong view
để thực hiện. Mọi thứ cần làm trong phương thức hoạt đông Menu là tạo
danh sách thể loại, đã đươc thực hiện trong Listing 8-7
Listing 8-7:Triển khai thực hiện các Menu Method trong NavController.cs
using System.Collections.Generic;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using System.Linq;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
private IProductRepository repository;
public NavController(IProductRepository repo) {
repository = repo;
}
public PartialViewResult Menu() {
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);


}
}
}

Thay đổi đầu tiên là thêm vào cấu trúc mà nó cho phép triển khai
IProductRepository như một đối số. Điều này ảnh hưởng tới sự phụ thuộc

mà Ninject sẽ giải đáp khi nó được tạo ra một trường của class
NavController. Thay đổi thứ 2 là về hoạt động phương thứcMenu, mà nó
bây giờ sử dụng vấn đáp LINQ để đạt được danh sách các thể loại từ trong
kho và chuyển chúng tới view. Chú ý, từ lúc tác giả làm việc với phần view
trong controller này, tác giả đã gọi phương thức PartialView trong những
phương thức hoạt động và nó là kết quả của đối tượng PartialViewResult.

UNIT TEST: GENERATING THE CATEGORYLIST
Các unit test cho thấy khả năng tạo ra những danh sách thể loại tương
đối đơn giản. Thành công của tác giả là tạo được một danh sách được sắp
xếp theo bảng chữ cái và không có sự trùng lặp. Cách đơn giản nhất để làm
điều này là cung cấp các dữ liệu thử nghiệm mà nó có sự trùng lặp về thể
loại và trong tình trạng không đươ đặt, chuyển đến NavController, và xác
nhận những dữ liệu đã đươc hoàn toàn được chuyển đi:
...
[TestMethod]
public void Can_Create_Categories() {
// 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"},
new Product {ProductID = 2, Name = "P2", Category = "Apples"},
new Product {ProductID = 3, Name = "P3", Category = "Plums"},
new Product {ProductID = 4, Name = "P4", Category = "Oranges"},
});
// Arrange - create the controller
NavController target = new NavController(mock.Object);
// Act = get the set of categories
string[] results =((IEnumerable<string>)target.Menu().Model).ToArray();

// Assert
Assert.AreEqual(results.Length, 3);
Assert.AreEqual(results[0], "Apples");
Assert.AreEqual(results[1], "Oranges");


Assert.AreEqual(results[2], "Plums");
}
...

Tác giả đã tạo ra một mô hình kho lưu trữ mà nó thuộc về những thể
loại được lặp lại và những thể loại đã được đặt. Tác giả xác nhận những sự
trùng lặp đã đươc loại bỏ và thứ tự xắp xếp theo chữ cái đươc nâng cao.

Khởi tạo View
Để tạo view cho phương thức hoạt động Menu, nhất chuột phải chọn
mục View/Nav và chọn Add->MVC 5 View Page (Razor) từ cửa sổ chọn.
Đặt tên là Menuvà nhấn OK để tạo Menu.cshtml. Loại bỏ những contents
mà Visual thêm vào cho view mới và đặt lại các content cho khớp với
Listing 8-8.
Listing 8-8:Những Content của file Menu.cshtml
@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product", null,
new { @class = "btn btn-block btn-default btn-lg" })
@foreach (var link in Model) {
@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1

}, new {
@class = "btn btn-block btn-default btn-lg"
})
}

Ở đây tác giả đã thêm đường dẫn gọi là Home để nó sẽ xuất hiện trên
top của danh sách thể loại và nó sẽ tổng hợp toàn bộ những sản phẩm mà
không cần dùng bộ lọc thể loại. Tác giả sử dụng phương thức hỗ trợ
ActionLink để thực hiện điều này, phương thức này sẽ tạo ra một phần tử
neo HTML sử dụng các thông tin định tuyết đươc cấu hình trước đó.
Sau đó tác giả liệt kê những tên thể loại và tạo ra đường dẫn đến từng
tên thể loại bằng cách sử dụng phương thức RouteLink. Nó gần giống như
ActionLink, nhưng nó cho phép tác giả cung cấp thiết lập về tên/ các cặp
giá trị mà nó được đưa vào account khi khởi tạo một URL từ thiết lặp định
tuyết. Không cần phải lo lắng nếu chưa hình dung ra được định tuyết này.
Mọi thứ được tác giả giải thích trong chương 15.


Đường dẫn mà tác giả tạo thì sẽ trông không đẹp nhưng là cơ bản
nhất, nên tác giả đã cung cấp thêm đối tượng cho cả hai phương thức hỗ trợ
ActionLink và RouteLinkmà nó xác định các giá trị cho các thuộc tính đã
đươc tạo ra. Đối tượng tác giả tạo để xác định giá trị của Class (tái sửa chữa
với @ bởi vì Class dành riêng cho C# keyword) và thêm vào class
Bootstrap để tạo kiểu cho các nút button lớn.
Bạn có thể thấy đường dẫn vào thể loại nếu bạn chạy ứng dụng, được
thể hiện ở Figure 8-3. Nếu bạn chọn vào category, thì danh sách những sản
phẩm đã được cập nhật sẽ chỉ thể hiện sản phẩm từ category đã chọn.
Figure 8-3:

Highlight danh mục hiện tại



Tại thời điểm hiện tại, tác giả chưa chỉ ra cho user category mà họ
đang xem.Nó có thể là bất cứ gì làm cho người customer suy luận từ những
sản phẩm trong danh sách, nhưng tác giả mong muốn cung cấp những phản
hồi thị giác. Tác giả có thể làm điều này bằng việc tạo một view model mà
nó thuộc về danh sách các thể loại và sự chọn lựa các thể loại, và thực tế là,
đó là những gì mà tác giả thường làm. Nhưng đối với phần nhiều tác giả đã
sử dụng tính năng view bag được giới thiệu ở chương 2. Chức năng này cho
phép tác giả chuyển dữ liệu từ controller tới view mà không cần dùng đến
view model. Listing 8-9 sẽ thể hiện những thay đổi của phương thức hoạt
động Menu trong controllerNav.
Listing 8-9: Sử dụng View Bag trong NavController.cs .
using System.Collections.Generic;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using System.Linq;
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
private IProductRepository repository;
public NavController(IProductRepository repo) {
repository = repo;
}
public PartialViewResult Menu(string category = null) {
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products
.Select(x => x.Category)
.Distinct()
.OrderBy(x => x);
return PartialView(categories);

}
}
}

Tác giả đã thêm vào thông số cho phương thức hoạt động Menu. Giá
trị cho thông số này sẽ được cung cấp một cách tự động bằng thông tin định
tuyến. Bên trong phần than của phương thức, tác giả đả gán động các tính
chất của SelectedCategorytới đối tượng ViewBagvà đặt cho giá trị của nó
là current category. Như tác giả đã giải thích ở chương 2, ViewBag là một
đối tượng động và tác giả tạo một tính chất đơn giản dựa trên việc đặt giá
trị cho chúng.

UNIT TEST: REPORTING THE SELECTED CATEGORY


Ở đây có thể kiểm tra phương thức hoạt động Menu một cách

chính xác cho biết thêm về giá trị của thể loại đươc chọn bằng việc
đọc giá trị trong những tính chất của ViewBagtrong các đơn vị
kiểm tra, là thứ mà sẽ hiện hữu thông qua class ViewRuslt. Đây là
bài test:

[TestMethod]
public void Indicates_Selected_Category() {
// 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"},
new Product {ProductID = 4, Name = "P2", Category = "Oranges"},

});
// Arrange - create the controller
NavController target = new NavController(mock.Object);
// Arrange - define the category to selected
string categoryToSelect = "Apples";
// Action
string result =target.Menu(categoryToSelect).ViewBag.SelectedCategory;
// Assert
Assert.AreEqual(categoryToSelect, result);
}


Unit test này sẽ không thể chạy được trừ khi bạn đã thêm vào sự liên
kết với thư viện Microsoft.CSharp như đã được miêu tả ở bài trước.
Bây giờ tác giả sẽ cung cấp thông tin cho biết category nào đã được
chọn, có thể cập nhật view để tạo thuận lợi cho việc này, và thêm vào class
CSS cho yếu tố neo HTML mà nó đại diện cho category đã được chọn.
Listing 8-10 sẽ thể hiện điều này.
Listing8-10: Làm nổi bật Selected Category trong file Menu.cshtml:
@model IEnumerable<string>
@Html.ActionLink("Home", "List", "Product", null,
new { @class = "btn btn-block btn-default btn-lg" })
@foreach (var link in Model) {


@Html.RouteLink(link, new {
controller = "Product",
action = "List",
category = link,
page = 1

}, new {
@class = "btn btn-block btn-default btn-lg"
+ (link == ViewBag.SelectedCategory ? " btn-primary" : "")
})
}

Thay đổi này là đơn giản. Nếu giá trị hiện tại của Link khớp với giá trị
của SelectedCategory, tác giả sẽ thêm vào thuộc tính mà tác giả đã tạo cho
một class Bootstrap khác, class Bootstrap này sẽ làm cho các button được
nổi bật. Chạy ứng dụng sẽ thể hiện sự ảnh hưởng của việc làm nổi bật
category.
Figure 8-4:

Correcting page count


Cần làm đúng các đường dẫn đến trang để khi chọn một category
chúng sẽ hoạt động đúng. Hiện tại, số trang sẽ phụ thuộc vào tổng số sản
phẩm có trong kho và không phải là số sản phẩm trong category đã đươc
chọn. Điều này có nghĩa là customer có thể chọn đường dẫn đến trang 2 của
category Chessvà sẽ hiện ra một trang trống từ đầu đến cuối bởi vì sẽ
không có đủ sản phẩm để có thể chia làm 2 trang.
Figure 8-5:

Có thể sửa lỗi này bằng cách cập nhật lại phương thức hoạt động
Listtrong controller Product để cho thông tin số trang sẽ đươc đưa vào
account từ các thể loại. Bạn có thể thấy những thay đổi ở Listing 8-11.
Listing 8-11: tạo Category-Aware Pageination Data trong file
ProductController.cs


public ViewResult List(string category, int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel {
Products = repository.Products
.Where(p => category == null || p.Category == category)
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {


CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = category == null ?
repository.Products.Count() :
repository.Products.Where(e => e.Category
==category).Count()
},
CurrentCategory = category
};
return View(viewModel);
}
...

Nếu mà category đã được chọn, thì sẽ trả về số sản phẩm có trong
category đó; nếu không, thì sẽ trả về tổng số của sản phẩm trong kho. Bây
giờ khi mà ta xem một category, đường dẫn ở cuối trang sẽ hiển thị đúng số
sản phẩm trong category đó như ở Figure 8-6.
Figure 8-6:



UNIT TEST: CATEGORY-SPECIFIC PRODUCTION COUNT
Bài test này cho phép tác giả có thể tạo số lượng sản phẩm khác nhau
một cách đơn giản. Tác giả tạo một kho lưu trữ giả chứa dữ liệu đươc biết
đến trong một loạt các category và sau đó yêu cầu gọi phương thức hoạt
động List từng category trong lượt.
...
[TestMethod]
public void Generate_Category_Specific_Product_Count() {
// 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 = "Cat1"},
new Product {ProductID = 2, Name = "P2", Category = "Cat2"},
new Product {ProductID = 3, Name = "P3", Category = "Cat1"},
new Product {ProductID = 4, Name = "P4", Category = "Cat2"},
new Product {ProductID = 5, Name = "P5", Category = "Cat3"}
});
// Arrange - create a controller and make the page size 3 items
ProductController target = new ProductController(mock.Object);
target.PageSize = 3;
// Action - test the product counts for different categories
int res1 = ((ProductsListViewModel)target
.List("Cat1").Model).PagingInfo.TotalItems;
int res2 = ((ProductsListViewModel)target
.List("Cat2").Model).PagingInfo.TotalItems;
int res3 = ((ProductsListViewModel)target
.List("Cat3").Model).PagingInfo.TotalItems;
int resAll = ((ProductsListViewModel)target
.List(null).Model).PagingInfo.TotalItems;

// Assert
Assert.AreEqual(res1, 2);
Assert.AreEqual(res2, 2);
Assert.AreEqual(res3, 1);
Assert.AreEqual(resAll, 5);
}
...

Chú ý: tác giả ở đây cũng đã gọi phương thức List,quy định cụ thể không
category, để chắc chắn rằng tác giả có được đúng số lượng đếm.

Xây dựng giỏ hàng


Ứng dụng đã có những bước tiến độc đáo, nhưng chưa thể bán đươc
sản phẩm cho đến khi nào giỏ hàng đươc đưa vào. Trong section này, tác giả
sẽ tạo ra những trãi nghiệm mua sắm với giỏ hàng, được trình bày trong
Figure 8-7. Điều này sẽ rất quen thuộc với những ai đã thực hiện thanh toán
trực tuyến.
Figure 8-7:

Nút Add to Cart sẽ được xuất hiện bên cạnh mỗi sản phẩm trong
catalog. Nhấn vào nút này sẽ thể thiện tổng số sản phẩm mà customer đã
được lựa chọn cho đến thời điểm đó, bao gồm cả tổng tiền. Lúc này,
customer sẽ có thể nhấn vào nút Continue Shopping để quay trở lại trang
danh mục sản phẩm, hoặc nhấn nút Checkout Now để hoàn tất việc đặt
hàng và kết thúc việc shopping.

Defining Cart Entity
Giỏ hàng là một phần trong lĩnh vực kinh doanh, vì vậy nó tạo cảm

giác dại diện cho giỏ hàng bằng cách tạo một thực thể trong domain model.
Thêm class với tên là Cart.cs vào thư mục Entities trong project
SportStore.Domain và dùng nó để định nghĩa class ở Listing 8-12
Listing 8-12:
using System.Collections.Generic;
using System.Linq;
namespace SportsStore.Domain.Entities {
public class Cart {
private List<CartLine> lineCollection = new List<CartLine>();


public void AddItem(Product product, int quantity) {
CartLine line = lineCollection
.Where(p => p.Product.ProductID == product.ProductID)
.FirstOrDefault();
if (line == null) {
lineCollection.Add(new CartLine { Product = product,
Quantity = quantity });
} else {
line.Quantity += quantity;
}
}

public void RemoveLine(Product product) {
lineCollection.RemoveAll(l => l.Product.ProductID
==product.ProductID);
}
public decimal ComputeTotalValue() {
return lineCollection.Sum(e => e.Product.Price * e.Quantity);
}

public void Clear() {
lineCollection.Clear();
}
public IEnumerable<CartLine> Lines {
get { return lineCollection; }
}
}
public class CartLine {
public Product Product { get; set; }
public int Quantity { get; set; }
}
}

Class CartLine được sử dụng cho class Cart, được định nghĩa chung
một tên, để đại diện cho sản phẩm đã được chọn bở customer và lượng lớn
người dùng muốn mua. Tác giả đã định nghĩa phương thức để có thê thêm sản
phẩm vào giỏ hàng, cũng như xóa sản phẩm đã thêm trước đó khỏi giỏ hàng,
tính tổng số tiền của sản phẩm trong giỏ hàng, tái tạo giỏ hàng bằng cách xóa
hết sản phẩm. Tác giả cũng cung cấp các quyền sở hữu cho phép truy cập vào
các nội dung của giỏ hàng bằng cách sử dụng Ienumrable<CartLine>. Đây
là các công cụ đơn giản, dễ dàng thực hiện trong C# với sự giúp đỡ cửa LINQ.


UNIT TEST: TESTING THE CART
Class Cart tương đối đơn giản, nhưng việc quan trọng là giỏ hàng phải
hoạt động đúng. Một giỏ hàng kém sẽ làm suy yếu toàn bộ ứng dụng
SportStore. Tác giả đã chia nhỏ các tính năng và thử nghiệm chúng riêng. Tác
giả đã tạo ra những file Unit Test trong project SportStore.UnitTes được gọi
là các CartTest.cs để chứa các bài kiểm tra.
Các hành vi đầu tiên quan khi mà thêm vào giỏ hàng. Nếu đây là lần thêm

sản phẩm đầu tiên vào giỏ hàng, tác giả muốn có một CartLine mới được bổ
sung. Dưới đây bài test, bao gồm cả việc định nghĩa lớp thử nghiệm đơn vị:
using System. Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SportsStore.Domain.Entities;
namespace SportsStore.UnitTests {
[TestClass]
public class CartTests {
[TestMethod]
public void Can_Add_New_Lines() {
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// Arrange - create a new cart
Cart target = new Cart();
// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
CartLine[] results = target.Lines.ToArray();
// Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Product, p1);
Assert.AreEqual(results[1].Product, p2);
}
}
}

Tác giả cũng cần kiểm tra là người dùng có thể thay đổi ý định và xóa bỏ
sản phẩm trong giỏ hàng. Việc này có thể giải quyết bằng phương thức
RemoveLine. Đây là bài test:




[TestMethod]
public void Can_Add_Quantity_For_Existing_Lines() {
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1" };
Product p2 = new Product { ProductID = 2, Name = "P2" };
// Arrange - create a new cart
Cart target = new Cart();
// Act
target.AddItem(p1, 1);
target.AddItem(p2, 1);
target.AddItem(p1, 10);
CartLine[] results = target.Lines.OrderBy(c =>c.Product.ProductID).ToArray();
// Assert
Assert.AreEqual(results.Length, 2);
Assert.AreEqual(results[0].Quantity, 11);
Assert.AreEqual(results[1].Quantity, 1);
}
...

Bài test cuối cùng tác giả muốn chắc chắn rằng mọi thứ liên quan đến giỏ
hàng sẽ được xóa bỏ khi reset. Đây là bài test:

[TestMethod]
public void Can_Clear_Contents() {
// Arrange - create some test products
Product p1 = new Product { ProductID = 1, Name = "P1", Price = 100M
};

Product p2 = new Product { ProductID = 2, Name = "P2", Price = 50M };
// Arrange - create a new cart
Cart target = new Cart();
// Arrange - add some items
target.AddItem(p1, 1);
target.AddItem(p2, 1);
// Act - reset the cart
target.Clear();
// Assert
Assert.AreEqual(target.Lines.Count(), 0);
}.
...


Đôi khi, trong trường hợp này, những code được yêu cầu cho phương
thức này thì dài và phức tạp hơn những gì nó mang lại. Đừng để điều đó làm
bạn bỏ qua việc viết unit test. Những khuyết điểm trong Class có thể có những
tác động lớn, đặt biệt là những thứ đóng vai trò quan trọng như Cart đến các ví
dụ của ứng dụng.

Adding the Add to Cart button


đây

tác

giả

cần


chỉnh
sửa
lớp
view
Views/Shared/ProductSummary.cshtml để có thể thêm button vào trong
danh sách sản phẩm. Thay đổi được thể hiện ở Listing 8-13:
Listing 8-13: Thêm button vào file view Product Summary.cshtml.
@model SportsStore.Domain.Entities.Product
<div class="well">


<strong>@Model.Name</strong>
@Model.Price.ToString("c")</span>


@using (Html.BeginForm("AddToCart", "Cart")) {
<div class="pull-right">
@Html.HiddenFor(x => x.ProductID)
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)

</div>
}
<span class="lead"> @Model.Description</span>
</div>

Tác giả đã thêm một Razor block mà nó tạo ra một đoạn HTML nhỏ ở
mỗi sản phẩm trong danh sách. Khi các form này đươc thông qua, nó sẽ gọi

phương thức hoạt động AddToCart trong controller Cart.
Chú ý: Ở mặc định, phương thức trợ giúp BeginForm tạo ra một form
mà sử dụng phương thức HTML POST. Bạn có thể thay đổi điều này bằng
cách cho sử dụng phương thức GET, nhưng cần suy nghĩ kĩ khi thực hiện điều
này. Các đặc điểm kỉ thuật của HTML yêu cầu rằng yêu cầu GET phải là
idempotent, nghĩ là chúng sẽ không được quền thay đổi, và thêm sản phẩm vào
giỏ hàng là một thay đổi xác định. Điều này sẽ được nhắc tới nhiều hơn ở


chương 16, kể cả việc giải thích những gì có thể xảy ra nếu bạn phớt lờ điều cần
có ở yêu cầu idempotent GET.

CREATING MULTIPLE HTML FORMS IN A PAGE
Sử dụng Html.BeginForm trợ giúp trong mỗi danh sách sản phẩm
nghĩa là mỗi nút Add to Cart thì kết xuất trong yếu tố HTML Form riêng biệt
của nó. Điều này có thể khiến bạn ngạc nhiên nếu như bạn đang phát triển với
nền tảng ASP.NET Web Forms mà chỉ có cho phép giới hạn 1 hình thức cho
mỗi trang nếu bạn sử dụng chức năng xem state feature hoặc điều khiển phức
tạp (có xu hướng dựa trên view state). Từ khi mà ASP.NET không còn sử dụng
view state, thì đã không còn giới hạn về hình thức mà bạn có thể tạo.
Công bằng hơn, không có công thức ( yêu cầu) nào cho việc tao một form
cho mỗi button. Tuy nhiên, bởi vì mỗi form sẽ đăng tải trở lại đúng phương thức
điều khiển đã có, nhưng với một số thông số khác biệt, nó rất tuyệt và là hướng
đơn giản để giải quyêt các nút button.

Implementing the Cart Controller
Tác giả cần một controller để giải quyết việc nhấn liên tục nút Add to

Cart. Tạo một controller mới được gọi là CartController trong project
SportStore.WebUI và chỉnh sửa những gì thuộc về nó sao cho khớp với

Listing 8-14
Listing 8-14: nội dung của CartController.cs
using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class CartController : Controller {
private IProductRepository repository;
public CartController(IProductRepository repo) {
repository = repo;
}
public RedirectToRouteResult AddToCart(int productId, stringreturnUrl) {
Product product = repository.Products
.FirstOrDefault(p => p.ProductID == productId);


×