现在的位置: 首页 > 综合 > 正文

【译著】第8章 SportsStore:导航与购物车 — 《精通ASP.NET MVC 3框架》

2013年04月04日 ⁄ 综合 ⁄ 共 38390字 ⁄ 字号 评论关闭

C H A P T E R 8
■ ■ ■

SportsStore: Navigation and Cart
SportsStore:导航与购物车

In the previous chapter, we set up the core infrastructure of the SportsStore application. Now we will use the infrastructure to add key features to the application, and you’ll start to see how the investment in the basic plumbing pays off. We will be able to add important customer-facing features simply and easily. Along the way, you’ll see some additional features that the MVC Framework provides.
在上一章中,我们建立了SportsStore应用程序的核心基础结构。现在,我们将利用这一基础结构把一些关键特性添加到该应用程序上。你将看到,上一章在构建基础结构方面的付出得到怎样的回报。我们能够简单而容易地添加面向客户的重要特性。通过这种方式,你还会看到MVC框架提供的一些附加特性。

Adding Navigation Controls
添加导航控件

The SportsStore application will be a lot more usable if we let customers navigate products by category. We will do this in three parts:
如果我们让客户通过产品分类(category)对产品进行导航,SportsStore应用程序将会更加适用得多。我们将从三个方面来做这件事:

  • Enhance the List action model in the ProductController class so that it is able to filter the Product objects in the repository.
    增强ProductController类中的List动作方法,以使它能够对过滤储库中的Product对象。
  • Revisit and enhance our URL scheme and revise our rerouting strategy.
    重新考察并增强URL方案,修订我们的路由策略。
  • Create the category list that will go into the sidebar of the site, highlighting the current category and linking to others.
    创建加入到网站工具条中的产品分类列表,高亮当前分类,并链接到其它分类。

Filtering the Product List
过滤产品列表

We are going to start by enhancing our view model class, ProductsListViewModel. We need to communicate the current category to the view in order to render our sidebar, and this is as good a place to start as any. Listing 8-1 shows the changes we made.
我们打算从增强视图模型类ProductsListViewModel开始。我们需要把当前分类传递给视图,以渲染我们的工具条,而且这是从事其它工作的一个很好的开端。清单8-1是我们所作的修改。

Listing 8-1. Enhancing the ProductsListViewModel Class
清单8-1. 增强ProductsListViewModel类

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

We added a new property called CurrentCategory. The next step is to update the ProductController class so that the List action method will filter Product objects by category and use the new property we added to the view model to indicate which category has been selected. The changes are shown in Listing 8-2.
我们添加了一个叫做CurrentCategory的新属性。下一步是更新ProductController类,以使List动作方法能通过分类来过滤Product对象,并用我们添加到视图模型的这个新属性来指示已选择了哪个分类。其修改如清单8-2所示。

Listing 8-2. Adding Category Support to the List Action Method
清单8-2. 对List动作方法添加分类支持

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 = repository.Products.Count() }, CurrentCategory = category }; return View(viewModel); }

We’ve made three changes to this method. First, we added a new parameter called category. This category is used by the second change, which is an enhancement to the LINQ query—if category isn’t null, only those Product objects with a matching Category property are selected. The last change is to set the value of the CurrentCategory property we added to the ProductsListViewModel class. However, these changes mean that the value of TotalItems is incorrectly calculated—we’ll fix this in a while.
我们已经对此方法作了三处修改。第一,我们添加了一个名为category的新参数。这个category由第二个修改来使用,以增强LINQ查询 — 如果category非空,则只选出与Category属性匹配的那些Product对象。最后一个修改是设置我们添加到ProductsListViewModel类上的CurrentCategory属性的值。然而,这些修改意味着会不正确地计算TotalIterms的值 — 我们一会儿修正它。

UNIT TEST: UPDATING EXISTING UNIT TESTS
单元测试:更新现有的单元测试

We have changed the signature of the List action method, which will prevent some of our existing unit test methods from compiling. To address this, pass null as the first parameter to the List method in those unit tests that work with the controller. For example, in the Can_Send_Pagination_View_Model test, the action section of the unit test becomes as follows:
我们已经修改了List动作方法的签名,这会阻碍已有的单元测试方法进行编译。为了修正它,在使用这个控制器的那些单元测试中,把null作为第一个参数传递给List方法。例如,在Can_Send_Pagination_View_Model测试中,单元测试的“动作”部分成为这样:

ProductsListViewModel result = (ProductsListViewModel)controller.List(null, 2).Model;

By using null, we receive all of the Product objects that the controller gets from the repository, which is the same situation we had before we added the new parameter.
通过使用null,我们接收控制器从存储库获取的全部Product对象,这与我们添加这个新参数之前的情况相同。

Even with these small changes, we can start to see the effect of the filtering. If you start the application and select a category using the query string, like this:
即使用这些微小的变化,我们也能够看出过滤的效果。如果你运行此应用程序,并用查询字串选择一个分类,像这样:

http://localhost:23081/?category=Soccer

you’ll see only the products in the Soccer category, as shown in Figure 8-1.
你就会只看到Soccer分类中的产品,如图8-1所示。

图8-1

Figure 8-1. Using the query string to filter by category
图8-1. 通过category使用查询字串进行过滤

UNIT TEST: CATEGORY FILTERING
单元测试:分类过滤

We need a unit test to properly test the category filtering function, to ensure that we can filter correctly and receive only products in a specified category. Here is the test:
我们需要一个单元测试来适当地测试分类的过滤功能,以确保能够正确地进行过滤,并且只接收指定分类中的产品。以下是这个测试:

[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"}
    }.AsQueryable());
// Arrange - create a controller and make the page size 3 items // 布置 — 创建一个控制器,并把页面大小设置为3个条目 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"); }

This test creates a mock repository containing Product objects that belong to a range of categories. One specific category is requested using the Action method, and the results are checked to ensure that the results are the right objects in the right order.
这个测试创建了一个模仿存储库,该存储库包含了一些属于各个分类的Product对象。在“动作”部分请求一个特定的分类,并检查其结果,以确认该结果是正确顺序的正确对象。

Refining the URL Scheme
细化URL方案

No one wants to see or use ugly URLs such as /?category=Soccer. To address this, we are going to revisit our routing scheme to create an approach to URLs that suits us (and our customers) better. To implement our new scheme, change the RegisterRoutes method in Global.asax to match Listing 8-3.
没人希望看到或使用像/?category=Soccer这种难看的URL。为了改善它,我们打算重新考察我们的路由方案,以创建一种更适合于我们(及我们的客户)的URL方法。为了实现这种新方案,修改Global.asax中的RegisterRoutes方法,使之符合清单8-3。

Listing 8-3. The New URL Scheme
清单8-3. 新的URL方案

public static void RegisterRoutes(RouteCollection routes) {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(null, "", // Only matches the empty URL (i.e. /)(只匹配空的URL(如,/) new { controller = "Product", action = "List", category = (string)null, page = 1 } );
routes.MapRoute(null, "Page{page}", // Matches /Page2, /Page123, but not /PageXYZ // 匹配“/page1”、“/page123”等,但不匹配“/pageXTZ” new { controller = "Product", action = "List", category = (string)null }, new { page = @"\d+" } // Constraints: page must be numerical // 约束:page必须是数字 );
routes.MapRoute(null, "{category}", // Matches /Football or /AnythingWithNoSlash // 匹配“/Football”,或“/<任何不带/的东西>” new { controller = "Product", action = "List", page = 1 } );
routes.MapRoute(null, "{category}/Page{page}", // Matches /Football/Page567 // 匹配“/Football/Page567” new { controller = "Product", action = "List" }, // Defaults(默认) new { page = @"\d+" } // Constraints: page must be numerical(约束:page必须是数字) );
routes.MapRoute(null, "{controller}/{action}"); }

■ Caution It is important to add the new routes in Listing 8-3 in the order they are shown. Routes are applied in the order in which they are defined, and you’ll get some odd effects if you change the order.
小心:清单8-3中重要的是按所示的顺序添加新路由。路由是按其定义的顺序来运用的,如果你改变了这种顺序,你会得到奇怪的效果。

Table 8-1 describes the URL scheme that these routes represent. We will explain the routing system in detail in Chapter 11.
表8-1描述了这些路由所表示的URL方案。我们将在第11章详细解释路由系统。

Table 8-1. Route Summary
表8-1. 路由摘要
URL Leads To
作用
/ Lists the first page of products from all categories
列出所有分类产品的第一页
/Page2 Lists the specified page (in this case, page 2), showing items from all categories
列出显示所有分类条目的指定页(这里是page2)
/Soccer Shows the first page of items from a specific category (in this case, the Soccer category)
显示指定分类条目中的第一页(这里是Soccer分类)
/Soccer/Page2 Shows the specified page (in this case, page 2) of items from the specified category (in this case, Soccer)
显示指定分类(这里是Soccer)条目的指定页(这里是page2)
/Anything/Else Calls the Else action method on the Anything controller
调用Anything控制器上的Else动作方法

The ASP.NET routing system is used by MVC to handle incoming requests from clients, but it also requests outgoing URLs that conform to our URL scheme and that we can embed in web pages. This way, we make sure that all of the URLs in the application are consistent.
ASP.NET路由系统是由MVC来使用的,以处理来自客户端的请求,但它也请求符合URL方案的输出URL,以使我们能够把这个输出URL嵌入在web页面中。这样,我们可以确保应用程序中的所有URL都是一致的。

■ Note We show you how to unit test routing configurations in Chapter 11.
注:我们将在第11章中向你演示如何单元测试路由配置。

The Url.Action method is the most convenient way of generating outgoing links. In the previous chapter, we used this help method in the List.cshtml view in order to display the page links. Now that we’ve added support for category filtering, we need to go back and pass this information to the helper method, as shown in Listing 8-4.
Url.Action方法是生成输出链接最方便的办法。在上一章中,我们为了显示页面连接,在List.cshtml视图中使用了这个辅助器方法。现在,我们已经添加了对分类过滤的支持,我们需要回过头来把这个信息传递给这个辅助器方法,如清单8-4所示。

Listing 8-4. Adding Category Information to the Pagination Links
清单8-4. 将分类信息添加到分页链接

@model SportsStore.WebUI.Models.ProductsListViewModel
@{ ViewBag.Title = "Products"; }
@foreach (var p in Model.Products) { Html.RenderPartial("ProductSummary", p); }
<div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x, category = Model.CurrentCategory})) </div>

Prior to this change, the links we were generating for the pagination links were like this:
在这个修改之前,我们为分页连接所生成的连接是这样的:

http://<myserver>:<port>/Page2

If the user clicked a page link like this, the category filter he applied would be lost, and he would be presented with a page containing products from all categories. By adding the current category, which we have taken from the view model, we generate URLs like this instead:
如果用户点击这样的页面链接,他所运用的分类过滤会不起作用,显示给他的将是一个包含所有分类产品的页面。通过添加从视图模型获取的当前分类,我们生成了如下所示的URL:

http://<myserver>:<port>/Chess/Page2

When the user clicks this kind of link, the current category will be passed to the List action method, and the filtering will be preserved. After you’ve made this change, you can visit a URL such as /Chess or /Soccer, and you’ll see that the page link at the bottom of the page correctly includes the category.
当用户点击这种链接时,当前分类将被传递给List动作方法,过滤就会起作用了。作了这些修改之后,你可以访问/Chess或/Soccer这样的URL,这就会看到页面底部的链接是正确地包含该分类的页面链接。

Building a Category Navigation Menu
建立分类导航菜单

We now need to provide the customers with a way to select a category. This means that we need to present them with a list of the categories available and indicate which, if any, they’ve selected. As we build out the application, we will use this list of categories in multiple controllers, so we need something that is self-contained and reusable.
现在我们需要给客户提供一种选择一个分类的方法。意即,我们需要表现一个可用分类列表,并指示出他们之中哪一个是被选择的。随着对应用程序的扩建,我们将在多个控制中使用这个分类列表,因此,我们需要做一些让它是自包含且可重用的事情。

The ASP.NET MVC Framework has the concept of child actions, which are perfect for creating items such as a reusable navigation control. A child action relies on the HTML helper method called RenderAction, which lets you include the output from an arbitrary action method in the current view. In this case, we can create a new controller (we’ll call ours NavController) with an action method (Menu, in this case) that renders a navigation menu and inject the output from that method into the layout.
ASP.NET MVC框架具有一种叫做子动作的概念,它对创建诸如可重用导航控件之类的事情特别理想。子动作依赖于叫做RenderAction的HTML辅助器方法,它让你能够在当前视图中包含一个任意动作方法的输出。在这里,我们可以创建一个新控制器(称之为NavController),它有一个动作方法(这里是Menu),它渲染一个导航菜单,并把此动作方法的输出注入到布局之中。

This approach gives us a real controller that can contain whatever application logic we need and that can be unit tested like any other controller. It’s a really nice way of creating smaller segments of an application while preserving the overall MVC Framework approach.
这种方法使我们拥有了一个真正的控制器,它能够包含我们所需的各种应用程序逻辑,并且能够像其它控制器一样被单元测试。这是保持MVC整体框架前提下,创建应用程序小型片段的一种很好的办法。

Creating the Navigation Controller
创建导航控制器

Right-click the Controllers folder in the SportsStore.WebUI project and select Add → Controller from the pop-up menu. Set the name of the new controller to NavController, select the Empty controller option from the Template menu, and click Add to create the class.
右击SportsStore.WebUI项目的Controllers文件夹,从弹出菜单选择“添加” → “控制器”。将此新控制器名设为NavController,在“模板”菜单中选择“空控制器”选项,点击“添加”创建这个类。

Remove the Index method that Visual Studio creates by default and add the Menu action method shown in Listing 8-5.
删除Visual Studio默认创建的Index方法,并添加如清单8-5所示的Menu动作方法。

Listing 8-5. The Menu Action Method
清单8-5. Menu动作方法

using System.Web.Mvc; 
namespace SportsStore.WebUI.Controllers {
public class NavController : Controller {
public string Menu() { return "Hello from NavController"; } } }

This method returns a canned message string, but it is enough to get us started while we integrate the child action into the rest of the application. We want the category list to appear on all pages, so we are going to render the child action in the layout. Edit the Views/Shared/_Layout.cshtml file so that it calls the RenderAction helper method, as shown in Listing 8-6.
该方法返回一个固定的消息字符串,但它足以让我们把这个子动作集成到应用程序的其余部分。我们希望分类列表出现在所有页面上,因此我们打算在布局中渲染这个子动作。编辑View/Shared/_Layout.cshtml文件,以使它调用RenderAction辅助方法,如清单8-6所示。

Listing 8-6. Adding the RenderAction Call to the Razor Layout
清单8-6. 将RenderAction调用添加到Razor布局

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
</head>
<body>
    <div id="header">
        <div class="title">SPORTS STORE</div>
    </div>
    <div id="categories">
        @{ Html.RenderAction("Menu", "Nav"); }
    </div>
    <div id="content">
        @RenderBody()
    </div>
</body>
</html>

We’ve removed the placeholder text that we added in Chapter 7 and replaced it with a call to the RenderAction method. The parameters to this method are the action method we want to call (Menu) and the controller we want to use (Nav).
我们已经去掉了第7章添加的占位文本,代之以调用RenderAction方法。该方法的参数是我们想调用的动作方法(Menu),和我们想使用的控制器(Nav)。

■ Note The RenderAction method writes its content directly to the response stream, just like the RenderPartial method introduced in Chapter 7. This means that the method returns void, and therefore can’t be used with a regular Razor @ tag. Instead, we must enclose the call to the method inside a Razor code block (and remember to terminate the statement with a semicolon). You can use the Action method as an alternative if you don’t like this code-block syntax.
注:RenderAction方法直接把它的内容写入到响应流,就像第7章所介绍的RenderPartial方法一样。意即,该方法返回void,因此不能用一个规则的Razor标签@。我们必须把这个调用封装在一个Razor代码块中(而且要记住以分号为语句结束符)。如果你不喜欢这种代码语法,你可以选用Action方法来代替。

If you run the application, you’ll see that the output of the Menu action method is included in every page, as shown in Figure 8-2.
如果你运行这个应用程序,你将看到每个页面都包含了这个Menu动作方法的输出,如图8-2所示。

图8-2

Figure 8-2. Displaying the output from the Menu action method
图8-2. 显示Menu动作方法的输出

Generating Category Lists
生成分类列表

We can now return to the controller and generate a real set of categories. We don’t want to generate the category URLs in the controller. We are going to use a helper method in the view to do that. All we need to do in the Menu action method is create the list of categories, which we’ve done in Listing 8-7.
现在我们回到这个控制器,并生成一组实际分类。我们不想在该控制器中生成分类的URL。我们打算在视图中使用一个辅助器方法来做这件事。在Menu动作方法中所要做的是创建分类列表,用清单8-7来实现。

Listing 8-7. Implementing the Menu Method
清单8-7. 实现Menu方法

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.WebUI.Models; 
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); } } }

The Menu action method is very simple. It just uses a LINQ query to obtain a list of category names and passes them to the view.
Menu动作方法很简单。它只使用一个LINQ查询来获得一个分类名的列表并把它传递给视图。

UNIT TEST: GENERATING THE CATEGORY LIST
单元测试:生成分类列表

The unit test for our ability to produce a category list is relatively simple. Our goal is to create a list that is sorted in alphabetical order and contains no duplicates. The simplest way to do this is to supply some test data that does have duplicate categories and that is not in order, pass this to the NavController, and assert that the data has been properly cleaned up. Here is the unit test we used:
产生分类列表能力的单元测试是相对比较简单。我们的目标是创建一个按字母顺序排列且无重复的列表。最简单的办法是提供的测试数据是有重复且无序的分类,把它传递给NavController,并断言该数据得到了适当的整理。以下是我们所用的单元测试:

[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"},
    }.AsQueryable());
// 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"); }

We created a mock repository implementation that contains repeating categories and categories that are not in order. We assert that the duplicates are removed and that alphabetical ordering is imposed.
我们创建了一个模仿存储库的实现,它包含了重复性且无序的分类。我们断言,去掉了重复,并实现了按字母排序。

Creating the Partial View
创建分部视图

Since the navigation list is just part of the overall page, it makes sense to create a partial view for the Menu action method. Right-click the Menu method in the NavController class and select Add View from the pop-up menu.
由于导航列表只是整个页面的一部分,故对Menu动作方法创建分部视图是有意义的。右击NavController类中的Menu方法,并从弹出菜单选择“添加视图”。

Leave the view name as Menu, check the option to create a strongly typed view, and enter IEnumerable<string> as the model class type, as shown in Figure 8-3.
保留视图名为Menu,选中“创建强类型视图”复选框,输入IEnumerable<string>作为模型类的类型,如图8-3所示。

图8-3

Figure 8-3. Creating the Menu partial view
图8-3. 创建Menu分部视图

Check the option to create a partial view. Click the Add button to create the view. Edit the view contents so that they match those shown in Listing 8-8.
选中“创建分部视图”复选框。点击“添加”按钮以创建这个视图。编辑该视图内容,使之与清单8-8吻合。

Listing 8-8. The Menu Partial View
清单8-8. Menu分部视图

@model IEnumerable<string> 
@{ Layout = null; }
@Html.ActionLink("Home", "List", "Product")
@foreach (var link in Model) { @Html.RouteLink(link, new { controller = "Product", action = "List", category = link, page = 1 }) }

We’ve added a link called Home that will appear at the top of the category list and will take the user back to the first page of the list of all products with no category filter. We did this using the ActionLink helper method, which generates an HTML anchor element using the routing information we configured earlier.
我们添加了一个叫做Home的连接,它出现在分类列表的顶部,并将用户带到无分类过滤情况下所有产品列表的第一页。这是用ActionLink辅助器方法来实现的,该方法用我们之前配置的路由信息生成了一个HTML锚点元素(超链接元素)。

We then enumerated the category names and created links for each of them using the RouteLink method. This is similar to ActionLink, but it lets us supply a set of name/value pairs that are taken into account when generating the URL from the routing configuration. Don’t worry if all this talk of routing doesn’t make sense yet—we explain everything in depth in Chapter 11.
然后我们枚举分类名,并用RouteLink方法为每个分类名创建了连接,但在根据路由配置生成URL时,它让我们针对性地提供了一组“名字/值”对。如果不能理解这里所说的路由含义,不用担心 — 我们会在第11章详细解释路由的方方面面。

The links we generate will look pretty ugly by default, so we’ve defined some CSS that will improve their appearance. Add the styles shown in Listing 8-9 to the end of the Content/Site.css file in the SportsStore.WebUI project.
默认情况下,我们生成的连接很丑陋,因此我们定义了一些CSS以改善它的外观。把清单8-9所示的样式加到SportsStore.WebUI项目Content/Site.css文件的尾部。

Listing 8-9. CSS for the Category Links
清单8-9. 用于分类链接的CSS

DIV#categories A
{
    font: bold 1.1em "Arial Narrow","Franklin Gothic Medium",Arial; display: block;
    text-decoration: none; padding: .6em; color: Black;
    border-bottom: 1px solid silver;
}
DIV#categories A.selected { background-color: #666; color: White; }
DIV#categories A:hover { background-color: #CCC; }
DIV#categories A.selected:hover { background-color: #666; }

You can see the category links if you run the application, as shown in Figure 8-4. If you click a category, the list of items is updated to show only items from the selected category.
如果运行访应用程序,你就能看到这些分类链接了,如图8-4所示。如果你点击一个分类,条目列表会作出更新,只显示所选分类的条目。

图8-4

Figure 8-4. The category links
图8-4. 分类链接

Highlighting the Current Category
高亮当前分类

At present, we don’t indicate to users which category they are viewing. It might be something that the customer could infer from the items in the list, but it is preferable to provide some solid visual feedback.
此刻,我们还没有给用户指明他们正在查看哪个分类。也许用户可以根据所列出的条目进行推断,但更好的是提供某种特定的视觉反馈。

We could do this by creating a view model that contains the list of categories and the selected category, and in fact, this is exactly what we would usually do. But instead, we are going to demonstrate the View Bag feature we mentioned in the Razor section of Chapter 5. This feature allows us to pass data from the controller to the view without using a view model. Listing 8-10 shows the changes to the Menu action method.
这件事我们可以通过创建一个含有分类列表和所选分类的视图模型来实现,而且事实上,这恰恰是我们通常的做法。但在这里,我们打算演示第5章在Razor章节所提到的View Bag(视图包)特性。该特性允许我们把控制器的数据传递给视图而不需要用视图模型。清单8-10演示了对Menu动作方法的修改。

Listing 8-10. Using the View Bag Feature
清单8-10. 使用视图包特性

public ViewResult Menu(string category = null) {
ViewBag.SelectedCategory = category;
IEnumerable<string> categories = repository.Products .Select(x => x.Category) .Distinct() .OrderBy(x => x);
return View(categories); }

We’ve added a parameter to the Menu action method called category. The value for this parameter will be provided automatically by the routing configuration. Inside the method body, we’ve dynamically created a SelectedCategory property in the ViewBag object and set its value to be the parameter value. In Chapter 5, we explained that ViewBag is a dynamic object, and we can create new properties simply by setting values for them.
我们给Menu动作方法添加了一个名为category的参数。这个参数的值将由路由配置自动提供。在方法体中,我们在ViewBag对象中动态地创建了一个SelectedCategory属性,并将它的值设置为这个参数的值。在第5章中,我们解释过ViewBag是一个动态对象,可以简单地通过为属性设置值的办法来创建新属性。

UNIT TEST: REPORTING THE SELECTED CATEGORY
单元测试:报告被选中分类

We can test that the Menu action method correctly adds details of the selected category by reading the value of the ViewBag property in a unit test, which is available through the ViewResult class. Here is the test:
通过在单元测试中读取ViewBag属性值,我们可以测试Menu动作方法正确添加了被选中分类的细节。以下是该测试:

[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"}, }.AsQueryable());
// 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); }

Notice that we don’t need to cast the property value from the ViewBag. This is one the advantages of using the ViewBag object in preference to ViewData.
注意,我们不需要转换ViewBag的属性值。这是用ViewBag对象优于ViewData的优点之一。

Now that we are providing information about which category is selected, we can update the view to take advantage of this, and add a CSS class to the HTML anchor element that represents the selected category. Listing 8-11 shows the changes to the Menu.cshtml partial view.
现在,我们提供了哪个分类被选中的信息,我们可以更新视图以利用这一信息,并把一个CSS的class加到表示被选中分类的HTML锚点元素。清单8-11显示了对Menu.cshtml分部视图的修改。

Listing 8-11. Highlighting the Selected Category
清单8-11. 高亮选中的分类

@model IEnumerable<string> 
@{ Layout = null; }
@Html.ActionLink("Home", "List", "Product")
@foreach (var link in Model) { @Html.RouteLink(link, new { controller = "Product", action = "List", category = link, page = 1 }, new { @class = link == ViewBag.SelectedCategory ? "selected" : null } ) }

We have taken advantage of an overloaded version of the RouteLink method, which lets us provide an object whose properties will be added to the HTML anchor element as attributes. In this case, the link that represents the currently selected category is assigned the selected CSS class.
我们利用了RouteLink方法的重载版本,它让我们提供一个对象,该对象的属性将作为HTML属性被添加到这个HTML锚点元素上。这里,表示当前被选中分类的连接被赋予了selected的CSS的class。

■ Note Notice that we used @class in the anonymous object we passed as the new parameter to the RouteLink helper method. This is not a Razor tag. We are using a C# feature to avoid a conflict between the HTML keyword class (used to assign a CSS style to an element) and the C# use of the same word (used to create a class). The @ character allows us to use reserved keywords without confusing the compiler. If we just called the parameter class (without the @), the compiler would assume we are defining a new C# type. When we use the @ character, the compiler knows we want to create a parameter in the anonymous type called class, and we get the result we need.
注:注意,我们在这个匿名对象中使用了@class,把它作为新参数传递给RouteLink辅助器方法。这不是一个Razor标签。我们使用的是一个C#特性,以避免HTML关键词class(用来把一个CSS样式赋给一个元素)与C#的同样关键词class(用来创建一个类)之间的冲突。@字符允许我们用保留关键词而不至使编译器产生混淆。如果我们只把这个参数写成class(不带@),编译器会假设我们正在定义一个新的C#类型。当我们使用@字符时,编译器就知道我们是想创建一个叫做class的匿名类型参数,于是我们得到了我们所需要的结果。

Running the application shows the effect of the category highlighting, which you can also see in Figure 8-5.
运行这个应用程序显示了分类高亮的效果,如图8-5所示。

图8-5

Figure 8-5. Highlighting the selected category
图8-5. 高亮选中的分类

Correcting the Page Count
修正页面计数

The last thing we need to do is correct the page links so that they work correctly when a category is selected.
我们要做的最后一件事是修正页面连接,以使它们在选择了一个分类时能正确地工作。

Currently, the number of page links is determined by the total number of products, not the number of products in the selected category. This means that the customer can click the link for page 2 of the Chess category and end up with an empty page because there are not enough chess products to fill the second page. You can see how this looks in Figure 8-6.
当前,页面链接的数目是由产品总数确定的,而不是由被选中分类中的产品数所确定。这意味着,客户可以点击Chess分类的第2页而终止于一个空白页面,因为没有足够的棋类产品来填充第二个页面。你可以在图8-6看到这种情况。

图8-6

Figure 8-6. Displaying the wrong page links when a category is selected
图8-6. 当一个分类被选中时显示错误的页面链接

We can fix this by updating the List action method in ProductController so that the pagination information takes the categories into account. You can see the required changes in Listing 8-12.
通过更新ProductController中的List动作方法,我们可以修正这种情况,以使分页信息把分类考虑进来。你可以在清单8-12中看到所需的修改。

Listing 8-12. Creating Category-Aware Pagination Data
清单8-12. 创建分类感应的分页数据

public ViewResult List(string category, int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel { Products = repository.Products .Where(p => category == null ? true : 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); }

If a category is selected, we return the number of items in that category; if not, we return the total number of products.
如果选中了一个分类,我们返回该分类中的条目数,如果没选,则返回产品总数。

UNIT TEST: CATEGORY-SPECIFIC PRODUCT COUNTS
单元测试:特定分类的产品数

Testing that we are able to generate the current product count for different categories is very simple—we create a mock repository that contains known data in a range of categories and then call the List action method requesting each category in turn. We will also call the List method specifying no category to make sure we get the right total count as well. Here is the unit test:
测试我们能够对不同的分类生成当前产品数是很简单的 — 创建一个模仿存储库,它含有一系列分类的已知数据,然后依次调用请求每个分类的List动作方法。我们也调用未指定分类的List方法,以确保也得到了正确的总数。以下是该单元测试:

[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"}
    }.AsQueryable());
// Arrange - create a controller and make the page size 3 items // 布置 — 创建控制器,并让页面大小为3个条目 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); }

Now when we view a category, the links at the bottom of the page correctly reflect the number of products in the category, as shown in Figure 8-7.
现在,当我们查看一个分类时,页面底部的链接正确地反映了该分类中的产品数目,如图8-7所示。

图8-7

Figure 8-7. Displaying category-specific page counts
图8-7. 显示特定分类的页面计数

Building the Shopping Cart
建立购物车

Our application is progressing nicely, but we can’t sell any products until we implement a shopping cart. In this section, we’ll create the shopping cart experience shown in Figure 8-8. This will be familiar to anyone who has ever made a purchase online.
我们的应用程序进展良好,但在没有实现购物车之前,还不能销售任何产品。在本章节中,我们将创建如图8-8所示的购物车体验。曾作过在线购物的人对它是熟悉的。

图8-9

Figure 8-8. The basic shopping cart flow
图8-8. 基本的购物车流程

An Add to cart button will be displayed alongside each of the products in our catalog. Clicking this button will show a summary of the products the customer has selected so far, including the total cost. At this point, the user can click the Continue shopping button to return to the product catalog, or click the Checkout now button to complete the order and finish the shopping session.
在一个分类中的每个产品的旁边都显示一个“Add to cart(加入购物车)”的按钮。点击这个按钮将显示该客户已选的产品摘要,包括总费用。在这里,客户可以点击“Continue shopping(继续购物)”按钮,以回到产品分类,或点击“Check out now(现在付费)”按钮来完成订购,并结束购物会话。

Defining the Cart Entity
定义购物车实体

A shopping cart is part of our application’s business domain, so it makes sense to represent a cart by creating an entity in our domain model. Add a class called Cart to the Entities folder in the SportsStore.Domain project. These classes are shown in Listing 8-13.
购物车是我们应用程序事务域的一部分,因此,在我们的域模型中创建一个表现购物车的实体是有意义的。在SportsStore.Domain项目中的Entities文件夹中添加一个名为Cart的类。这些类如清单8-13所示。

Listing 8-13. The Cart Domain Entity
清单8-13. 购物车域实体

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

The Cart class uses CartLine, defined in the same file, to represent a product selected by the customer and the quantity the user wants to buy. We have defined methods to add an item to the cart, remove a previously added item from the cart, calculate the total cost of the items in the cart, and reset the cart by removing all of the selections. We have also provided a property that gives access to the contents of the cart using an IEnumerable<CartLine>. This is all straightforward stuff, easily implemented in C# with the help of a little LINQ.
这个Cart类使用了在同一个文件中定义的CartLine,来表示由客户选择的一个产品和用户想要购买的数量。我们定义了一些方法,包括:把一个条目添加到购物车、从购物车中删除之前加入的条目、计算购物车条目总费用、以及删除全部选择重置购物车等。我们也提供了一个属性,它使用IEnumerable<CartLine>对购物车的内容进行访问。所有这些都很直观,利用一点点LINQ的辅助,很容易用C#来实现。

UNIT TEST: TESTING THE CART
单元测试:测试购物车

The Cart class is relatively simple, but it has a range of important behaviors that we must ensure work properly. A poorly functioning cart would undermine the entire SportsStore application. We have broken down the features and tested them individually.
Cart类相对简单,但它有一些我们必须确保能正确工作的行为。贫乏的购物车功能会破坏整个SportsStore应用程序。我们已经分解了这些特性,并分别对它们进行测试。

The first behavior relates to when we add an item to the cart. If this is the first time that a given Product has been added to the cart, we want a new CartLine to be added. Here is the test:
第一个行为关系到我们把一个条目添加到购物车的时候。如果这是第一次把一个给定的Product添加到购物车,我们希望增加一个新的CartLine。以下是该测试:

[TestMethod]
public void Can_Add_New_Lines() {
    // Arrange - create some test products
    // 布置 — 创建一些测试用的product
    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); }

However, if the customer has already added a Product to the cart, we want to increment the quantity of the corresponding CartLine and not create a new one. Here is the test:
然而,如果客户已经把一个Product加到了购物车,我们希望增加相应CartLine的数量,而不要创建一个新的CartLine对象。以下是该测试:

[TestMethod]
public void Can_Add_Quantity_For_Existing_Lines() {
    // Arrange - create some test products
    // 布置 — 创建一些测试用product
    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); }

We also need to check that users can change their mind and remove products from the cart. This feature is implemented by the RemoveLine method. Here is the test:
我们也需要测试用户改变主意,并从购物车删除产品的行为。这一特性是由RemoveLine方法来实现的。以下是该测试:

[TestMethod]
public void Can_Remove_Line() {
    // Arrange - create some test products
    // 布置 — 创建一些测试用product
    Product p1 = new Product { ProductID = 1, Name = "P1" };
    Product p2 = new Product { ProductID = 2, Name = "P2" };
    Product p3 = new Product { ProductID = 3, Name = "P3" };
// Arrange - create a new cart // 布置 — 创建一个新购物车 Cart target = new Cart();
// Arrange - add some products to the cart // 布置 — 把一些产品添加到购物车 target.AddItem(p1, 1); target.AddItem(p2, 3); target.AddItem(p3, 5); target.AddItem(p2, 1);
// Act // 动作 target.RemoveLine(p2);
// Assert // 断言 Assert.AreEqual(target.Lines.Where(c => c.Product == p2).Count(), 0); Assert.AreEqual(target.Lines.Count(), 2); }

The next behavior we want to test is our ability to calculate the total cost of the items in the cart. Here’s the test for this behavior:
我们想要测试的下一个行为是计算购物车中各条目总费用的能力。以下是用于该行为的测试:

[TestMethod]
public void Calculate_Cart_Total() {
    // Arrange - create some test products
    // 布置 — 创建一些测试用product
    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();
// Act // 动作 target.AddItem(p1, 1); target.AddItem(p2, 1); target.AddItem(p1, 3); decimal result = target.ComputeTotalValue();
// Assert // 断言 Assert.AreEqual(result, 450M); }

The final test is very simple. We want to ensure that the contents of the cart are properly removed when we reset it. Here is the test:
最后一个测试很简单。我们希望在重置购物车时,恰当地删除了购物车的内容。以下是该测试:

[TestMethod]
public void Can_Clear_Contents() {
    // Arrange - create some test products
    // 布置 — 创建一些测试用product
    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); }

Sometimes, as in this case, the code required to test the functionality of a type is much longer and much more complex than the type itself. Don’t let that put you off writing the unit tests. Defects in simple classes, especially ones that play such an important role as Cart does in our application, can have huge impacts.
有时,正如上述情况一样,测试一个类型的功能所需的代码比该类型本身要长得多且复杂得多。不要让这种情况让你放弃单元测试。在简单类中的缺陷,尤其像这种在我们应用程序中起着重要作用的购物车如果有缺陷,有可能会产生巨大的影响。

Adding the Add to Cart Buttons
添加Add to Cart按钮

We need to edit the Views/Shared/ProductSummary.cshtml partial view to add the buttons to the product listings. The changes are shown in Listing 8-14.
我们需要编辑Views/Shared/ProductSummary.cshtml分部视图,以把这些按钮添加到产品列表。清单8-14显示了所作的修改。

Listing 8-14. Adding the Buttons to the Product Summary Partial View
清单8-14. 产品摘要分部视图上添加按钮

@model SportsStore.Domain.Entities.Product
<div class="item">
    <h3>@Model.Name</h3>
    @Model.Description
@using(Html.BeginForm("AddToCart", "Cart")) { @Html.HiddenFor(x => x.ProductID) @Html.Hidden("returnUrl", Request.Url.PathAndQuery) <input type="submit" value="+ Add to cart" /> }
<h4>@Model.Price.ToString("c")</h4> </div>

We’ve added a Razor block that creates a small HTML form for each product in the listing. When this form is submitted, it will invoke the AddToCart action method in the Cart controller (we’ll implement this method in just a moment).
我们对列表中的每个产品添加了一个Razor代码块,它创建一个小型表单(Form)。当这个表单被递交时,它将请求Cart控制器中的AddToCart动作方法(我们一会儿就会实现这个方法)。

■ Note By default, the BeginForm helper method creates a form that uses the HTTP POST method. You can change this so that forms use the GET method, but you should think carefully about doing so. The HTTP specification requires that GET requests must be idempotent, meaning that they must not cause changes, and adding a product to a cart is definitely a change. We’ll have more to say on this topic in Chapter 9, including an explanation of what can happen if you ignore the need for idempotent GET requests.
注:默认地,BeginForm辅助方法创建一个使用HTTP POST方法的表单。你可以对之进行修改,以使表单使用GET方法,但你这么做时应该仔细考虑。HTTP规范要求GET请求必须是幂等的,意即,它们必须不会引起变化,而把一个产品添加到购物车显然是一个变化(所以我们没用GET — 译者注)。关于这一论题,我们在第9章会有更多论述,并解释如果你对幂等的GET请求忽略了这种需求会发生什么。

We want to keep the styling of these buttons consistent with the rest of the application, so add the CSS shown in Listing 8-15 to the end of the Content/Site.css file.
我们希望这些按钮的样式与应用程序的其余部分一致,因此,把清单8-15所示的CSS样式加到Content/Site文件的尾部。

Listing 8-15. Styling the Buttons
清单8-15. 设置按钮样式

FORM { margin: 0; padding: 0; }
DIV.item FORM { float:right; }
DIV.item INPUT {
    color:White; background-color: #333; border: 1px solid black; cursor:pointer;
}

CREATING MULTIPLE HTML FORMS IN A PAGE
在一个页面中创建多个HTML表单

Using the Html.BeginForm helper in each product listing means that every Add to cart button is rendered in its own separate HTML form element. This may be surprising if you’ve been developing with ASP.NET Web Forms, which imposes a limit of one form per page. ASP.NET MVC doesn’t limit the number of forms per page, and you can have as many as you need.
在每个产品列表中使用Html.BeginForm辅助方法,意味着“Add to cart(加入购物车)”按钮会被渲染成它自己独立的HTML的form元素。如果你一直是用ASP.NET的Web表单从事开发,这可能是很奇怪的事情,因为Web表单具有每个页面只有一个表单的限制。ASP.NET MVC并不限制每页表单的个数,你可以有所需要的任意多个。

There is no technical requirement for us to create a form for each button. However, since each form will postback to the same controller method, but with a different set of parameter values, it is a nice and simple way to deal with the button presses.
对我们而言,为每个按钮创建一个表单并不是技术上的要求。然而,由于每个表单将会回递给同一个控制器方法,但却带有了一组不同的参数值,所以,这是处理按钮点击的一种很好而简单的方式。

Implementing the Cart Controller
实现购物车控制器

We need to create a controller to handle the Add to cart button presses. Create a new controller called CartController and edit the content so that it matches Listing 8-16.
我们需要创建一个控制器来处理“Add to cart(加入购物车)”按钮的点击。创建一个名为CartController的新控制器,并编辑其内容,使之与清单8-16吻合。

Listing 8-16. Creating the Cart Controller
清单8-16. 创建购物车控制器

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, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId); if (product != null) { GetCart().AddItem(product, 1); } return RedirectToAction("Index", new { returnUrl }); }
public RedirectToRouteResult RemoveFromCart(int productId, string returnUrl) { Product product = repository.Products .FirstOrDefault(p => p.ProductID == productId);
if (product != null) { GetCart().RemoveLine(product); } return RedirectToAction("Index", new { returnUrl }); }
private Cart GetCart() { Cart cart = (Cart)Session["Cart"]; if (cart == null) { cart = new Cart(); Session["Cart"] = cart; } return cart; } } }

There are a few points to note about this controller. The first is that we use the ASP.NET session state feature to store and retrieve Cart objects. This is the purpose of the GetCart method. ASP.NET has a nice session feature that uses cookies or URL rewriting to associate requests from a user together, to form a single browsing session. A related feature is session state, which allows us to associate data with a session. This is an ideal fit for our Cart class. We want each user to have his own cart, and we want the cart to be persistent between requests. Data associated with a session is deleted when a session expires (typically because a user hasn’t made a request for a while), which means that we don’t need to m

抱歉!评论已关闭.