【译著】第7章 SportsStore:一个真实的应用程序 — 《精通ASP.NET MVC 3框架》

C H A P T E R  7
■ ■ ■

SportsStore: A Real Application

We’ve built a quick, simple MVC application. We’ve looked at the MVC pattern. We’ve refreshed our memories about the essential C# features and tools that good MVC developers require. Now it’s time to put everything together and build a realistic e-commerce application.

Our application, SportsStore, will follow the classic approach taken by online stores everywhere. We’ll create an online product catalog that customers can browse by category and page, a shopping cart where users can add and remove products, and a checkout where customers can enter their shipping details. We’ll also create an administration area that includes create, read, update, and delete (CRUD) facilities for managing the catalog—and we’ll protect it so that only logged-in administrators can make changes.
我们的应用程序,SportsStore(体育用品商店),将遵循随处可见的在线商店所采取的经典方式。我们将创建一个客户可以通过分类和页面进行浏览的在线产品分类、一个客户可以添加和删除商品的购物车、和一个客户能够输入其邮寄地址细节的结算页面。我们还将创建一个包含创建、读取、更新、和删除(CRUD)功能的管理区,以便对产品分类进行管理 — 并对该区域进行保护,以使得只有登录的管理员才能够进行修改。

注:CRUD是指对数据库的常规操作:创建(Create)、读取(Read)、更新(Update)、删除(Delete) — 译者注

The application we are going to build isn’t just a shallow demonstration. Instead, we are going to create a solid and realistic application that adheres to current best practices. You might find the going a little slow as we build up the levels of infrastructure we need. Certainly, you would get the initial functionality built more quickly with Web Forms, just by dragging and dropping controls bound directly to a database. But the initial investment in an MVC application pays dividends, giving us maintainable, extensible, well-structured code with excellent support for unit testing. We’ll be able to speed up things once we have the basic infrastructure in place.


We’ve made quite a big deal about the ease of unit testing in MVC, and about our belief that unit testing is an important part of the development process. You’ll see this belief demonstrated throughout this book because we’ve included details of unit tests and techniques as they relate to key MVC features.

But we know this isn’t a universal belief. If you don’t want to unit test, that’s fine with us. So, to that end, when we have something to say that is purely about unit testing or TDD, we will put it in a sidebar like this one. If you are not interested in unit testing, you can skip right over these sections, and the SportsStore application will work just fine. You don’t need to do any kind of unit testing to get the benefits of ASP.NET MVC.
但我们知道,这并不是一种普遍的信念。如果你不想进行单元测试,那也很好。因此,说到底,我们是在纯粹地介绍单元测试或TDD(测试驱动开发),我们将把它作为我们手边的一种工具。如果你对单元测试不感兴趣,你可以跳过这些章节,SportsStore应用程序一样会工作得很好。你不一定要做任何单元测试来获得ASP.NET MVC的这种好处。

Some of the MVC features we are going to use have their own chapters later in the book. Rather than duplicate everything here, we’ll tell you just enough to make sense for this application and point you to the other chapter for in-depth information.

We’ll call out each step that is needed to build the application, so that you can see how the MVC features fit together. You should pay particular attention when we create views. You can get some odd results if you don’t use the same options that we use. To help you with this, we have included figures that show the Add View dialog each time we add a view to the project.

Getting Started

You will need to install the software described in Chapter 2 if you are planning to code the SportsStore application on your own computer as we go. You can also download SportsStore as part of the code archive that accompanies this book (available in the Source Code/Download area of www.apress.com). We have included snapshots of the application project after we added major features, so you can see how the application evolves as it is being built.
如果你打算跟着我们在你的计算机上编写SportStore应用程序的代码,你需要安装第2章所描述的软件。你也可以下载本书伴随代码文档中的SportsStore部分(www.apress.com的“Source Code/Download(源代码/下载)”区)。在对项目添加主要特性时,书中都包括了此应用程序项目的截图,因此你可以看到应用程序如何一步步演变成它所建成的样子。

You don’t need to follow along, of course. We’ve tried to make the screenshots and code listings as easy to follow as possible, just in case you are reading this book on a train, in a coffee shop, or the like.

Creating the Visual Studio Solution and Projects
创建Visual Studio解决方案和项目

We are going to create a Visual Studio solution that contains three projects. One project will contain our domain model, one will be our MVC application, and the third will contain our unit tests. To get started, let’s create an empty solution using the Visual Studio Blank Solution template, which you’ll find under the Other Project Types, Visual Studio Solutions section of the New Project dialog, as shown in Figure 7-1.
我们打算创建一个含有三个项目的Visual Studio解决方案。一个项目包含域模型、一个是MVC应用程序、而第三个则包含了单元测试。为了能够开始工作,我们用Visual Studio的“空白解决方案(Blank Solution)”模板创建一个空的解决方案,该模板位于“新建项目”对话框中“其它项目类型”的“Visual Studio解决方案”小节,如图7-1所示。


Figure 7-1. Creating a blank solution
图7-1. 创建一个空白解决方案

Give your solution the name SportsStore and click the OK button to create it. Once you’ve created the solution, you can add the individual projects. The details of the three projects we need are shown in Table 7-1.

Table 7-1. The Three SportsStore Projects
表7-1. 三个SportsStore项目
Project Name
Visual Studio Project Template
Visual Studio项目模板
SportsStore.Domain C# Class Library
Holds the domain entities and logic; set up for persistence via a repository created with the Entity Framework
保存域实体和逻辑,通过用Entity Framework(实体框架)创建的存储库建立持久化
SportsStore.WebUI ASP.NET MVC 3 Web Application (choose Empty when prompted to choose a project template, and select Razor for the view engine)
ASP.NET MVC 3 Web应用程序(当提示选择项目模板时,选空白模板,并选择Razor作为视图引擎)
Holds the controllers and views; acting as the UI for the SportsStore application
SportsStore.UnitTests Test Project
Holds the unit tests for the other two projects
(注意:项目名称是SportsStore.Domain,而不是Domain等 — 译者注)

To create each of these projects, click the SportsStore solution in the Solution Explorer window, select Add → New Project, and select the template specified in the table. The Test Project template isn’t in the Test Projects section; you’ll find it in the Test category in the Visual C# group, as shown in Figure 7-2.
要创建各个项目,在“解决方案资源管理器”窗口中右击“SportsStore解决方案”,选择“添加”→“新建项目”,并选择表中所指定的模板。“测试项目”模板不在“测试项目”小节,你会在“Visual C#”分组中的“测试”中找到它,如图7-2所示。


Figure 7-2. Creating the unit test project
图7-2. 创建单元测试项目

Visual Studio will create a couple of files that we won’t use and that you can delete: the Class1.cs file in the SportsStore.Domain project and the UnitTest1.cs class in the SportsStore.UnitTests project. When you are finished, your Solution Explorer window should look like the one shown in Figure 7-3.
Visual Studio会创建两个我们用不到的、你可以删除的文件:SportsStore.Domain项目中的Class1.cs文件,和SportsStore.UnitTests项目中的UnitTest1.cs类。当完成上述工作之后,你的“解决方案资源管理器”窗口应该看上去如图7-3所示。


Figure 7-3. The projects shown in the Solution Explorer window
图7-3. 在解决方案资源管理器中显示的项目

To make debugging easier, right-click the SportsStore.WebUI project and select Set as Startup Project from the pop-up menu (you’ll see the name turn bold). This means that when you select Start Debugging or Start without Debugging from the Debug menu, it is this project that will be started.
为了使调试更容易些,右击SportsStore.WebUI项目,并从弹出菜单中选择“设为启动项目”(你将看到其名字成为粗体)。意即,当你从“调试”菜单中选择“启动调试(Start Debugging)”或“开始执行(不调试)(Start without Debugging)”时,它是应用程序的启动项目。

Adding References

We need to add references to the tool libraries we’re going to use. The quickest way to obtain and reference these is by opening the Visual Studio Package Manager Console (View → Other Windows → Package Manager Console), and entering the following commands. Remember you can press Tab to autocomplete the names of the commands, and even the packages themselves.
我们需要把要用到的引用添加到工具库中。获取和引用它们的最快方法是打开Visual Studio的Package Manager Console(包管理器控制台)(“视图”→“其它窗口”→“Package Manager Console”),并输入以下命令。记住,你可以按Tab键来自动完成命令名,甚至包名。

Install-Package Ninject -Project SportsStore.WebUI
Install-Package Ninject -Project SportsStore.Domain
Install-Package Moq -Project SportsStore.WebUI
Install-Package Moq -Project SportsStore.Domain

Or, if you prefer, you can download Ninject and Moq from their project web sites, and then manually add the references shown in Table 7-2. We also need to set up dependencies between our projects, as listed in the table.

Table 7-2. Required Project Dependencies
表7-2. 所需的项目依赖性
Project Name
Tool Dependencies
Project Dependencies
SportsStore.Domain None None
SportsStore.WebUI Ninject SportsStore.Domain
SportsStore.UnitTests Ninject
(注意,SportsStore.WebUI中也要引用Moq工具包,因为实现清单7-5时需要用到 — 译者注)

Right-click each project in the Solution Explorer window, select Add Reference, and add the reference to the tool library or one of the other projects as required.

Setting Up the DI Container

We are going to use Ninject to create our MVC application controllers and handle the DI. To do this, we need to create a new class and make a configuration change.

Create a new folder within the SportsStore.WebUI project called Infrastructure, then create a class called NinjectControllerFactory and edit the class file so that it matches Listing 7-1. This is very similar to the class we showed you in the “Applying Ninject to ASP.NET MVC” section of Chapter 6.
在SportsStore.WebUI项目中创建一个名为Infrastructure的文件夹,然后创建一个名为NinjectControllerFactory(Ninject控制器工厂)的类,并编辑这个类文件,使之与清单7-1相符。它与第6章“将Ninject运用于ASP.NET MVC”小节中所演示的类非常类似。

Caution Throughout this chapter (and indeed the rest of the book), we usually won’t give you explicit instructions when you need to add a using statement to bring a namespace into scope. To do so would be repetitious and take a lot of space, and it’s pretty easy to figure it out. For example, if Visual Studio underlines a class name in a code file and warns you that “The type or namespace Product could not be found,” it should be obvious that you need to add a using statement to bring the SportsStore.Domain.Entities namespace into scope in your class. The best way of doing this is to position the cursor above the type that is causing the error and press Control+. (dot). Visual Studio will figure out which namespace is required and pop up a menu that will let you add the using statement automatically. We will give you explicit instructions if you need to add a reference to an assembly in order to find a type.
小心:整个这一章(以及本书的其余部分),在需要添加一条using语句,以便把一个命名空间引入范围时,我们通常不会给出明确的说明。否则将显得重复并占用很多篇幅,而且,这种问题很容易解决。例如,如果Visual Studio在一个代码文件中的Product类名下有一条下划线,并警告你,“未找到Product的类型或命名空间”,这显然是需要你添加一条using语句,以便把SportsStore.Domain.Entities命名空间纳入到这个类(代码文件相应的类 — 译者注)的范围中来。做这件事最好的办法是把光标定位到引起错误的这个类型上,按Ctrl + .(点)。Visual Studio会判断出需要哪个命名空间,并自动弹出让你添加using语句的菜单。如果需要你添加对一个程序集的引用,以找到一个类型时,我们会给出明确的说明。

Listing 7-1. The NinjectControllerFactory Class
清单7-1. NinjectControllerFactory类

using System;
using System.Web.Mvc;
using System.Web.Routing;
using Ninject;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectControllerFactory : DefaultControllerFactory {
private IKernel ninjectKernel;
public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); }
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) {
return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); }
private void AddBindings() { // put additional bindings here // 这里放置其它绑定 } } }

We haven’t added any Ninject bindings yet, but we can use the AddBindings method when we are ready to do so. We need to tell MVC that we want to use the NinjectController class to create controller objects, which we do by adding the statement shown in bold in Listing 7-2 to the Application_Start method of Global.asax.cs in the SportsStore.WebUI project.

Listing 7-2. Registering the NinjectControllerFactory with the MVC Framework
清单7-2. 注册MVC框架使用的NinjectControllerFactory

protected void Application_Start() {
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory()); }

Starting the Application

If you select Start Debugging from the Debug menu, you’ll see an error page. This is because you’ve requested a URL that’s associated with a controller that Ninject doesn’t have a binding for, as shown in Figure 7-4.


Figure 7-4. The error page
图7-4. 错误页面

If you’ve made it this far, your Visual Studio 2010 and ASP.NET MVC development setup is working as expected. If your default browser is Internet Explorer, you can stop debugging by closing the browser window. Alternatively, you can switch back to Visual Studio and select Stop Debugging from the Debug menu.
如果你已经走到了这里,说明你的Visual Studio 2010和ASP.NET MVC开发环境的准备工作进行得十分顺利。如果你的默认浏览器是Internet Explorer,可以关闭浏览器窗口来停止调试。否则,你可以切换回Visual Studio,然后从“调试”菜单中选择“停止调试”。

Easier Debugging

When you run the project from the Debug menu, Visual Studio will create a new browser window to display the application. As a speedier alternative, you can keep your application open in a stand-alone browser window. To do this, assuming you have launched the debugger at least once already, right-click the ASP.NET Development Server icon in the system tray and choose Open in Web Browser from the pop-up window, as shown in Figure 7-5.
当你通过“调试”菜单运行项目时,Visual Studio会创建一个新的浏览器窗口来显示此应用程序。一种较快的办法是,可以让应用程序在一个独立的浏览器窗口中打开。假设你至少已经运行了一次调试,可以右击“系统托盘”(屏幕右下角 — 译者注)中的“ASP.NET开发服务器(ASP.NET Development Server)”图标,并从弹出菜单中选择“在web浏览器中打开(Open in Web Browser)”,如图7-5所示。


Figure 7-5. Starting the application without using the debugger
图7-5. 不使用调试器启动应用程序

This way, each time you make a change to the application, you won’t need to launch a new debugging session to see the effect. You simply compile the solution in Visual Studio by pressing F6 or choosing Build → Build Solution, and then switch to your browser window and reload the web page.
这样,每次对应用程序作了修改之后,你不需要运行新的调试来查看效果。只要编译Visual Studio中的解决方案(按F6,或选择“创建(Build)” → “创建解决方案(Build Solution)”),然后切换到刚才的浏览器窗口,并重载该web页面(在浏览器中“刷新”页面,或按F5键 — 译者注)。

Starting the Domain Model

We are going to start with the domain model. Pretty much everything in an MVC application revolves around the domain model, so it is the perfect place to start.

Since this is an e-commerce application, the most obvious domain entity we’ll need is a product. Create a new folder called Entities inside the SportsStore.Domain project and then a new C# class called Product within it. You can see the structure we are looking for in Figure 7-6.


Figure 7-6. Creating the Product class
图7-6. 创建Product类

You are already familiar with the contents of the Product class, as we are going to use the same class you saw in the previous chapters. It contains the obvious properties that we need. Edit your Product class file so that it matches Listing 7-3.

Listing 7-3. The Product Class File
清单7-3. Product类文件

namespace SportsStore.Domain.Entities {
public class Product {
public int ProductID { get; set; } public string Name { get; set; } public string Description { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }

We have followed the convention of defining our domain model in a separate Visual Studio project, which means that the class must be marked as public. You don’t need to follow this convention, but we find that it helps us keep the model separate from the controllers.
我们已经遵循了在一个独立的Visual Studio项目中定义域模型的约定,即,类必须标记为public。虽然你不一定要遵守这一约定,但我们发现,这么做有助于保持模型与控制器分离。

Creating an Abstract Repository

We know that we need some way of getting Product entities from a database. As we explained in Chapter 4, we want to keep the persistence logic separate from the domain model entities—and we do this by using the repository pattern. We don’t need to worry about how we are going to implement the persistence for the moment, but we will start the process of defining an interface for it.
我们知道,我们需要某种从数据库中获取Product实体的方法。正如在第4章所解释的那样,我们希望把持久化逻辑从域模型实体中分离出来 — 并通过使用存储库模式来实现这一点。我们此时不必担心如何去实现持久化,但我们将为它定义一个接口来开始这一过程。

Create a new top-level folder inside the SportsStore.Domain project called Abstract and a new interface called IProductRepository, the contents of which are shown in Listing 7-4. You can add a new interface by right-clicking the Abstract folder, selecting Add → New Item, and selecting the Interface template.
在SportsStore.Domain项目中创建一个新的名为Abstract的顶级文件夹,以及一个名为IProductRepository的新接口,其内容如清单7-4所示。通过右击“Abstract”文件夹 → “添加新项” → 选“接口”模板,你可以添加一个新接口。

Listing 7-4. The IProductRepository Interface File
清单7-4. IProductRepository接口文件

using System.Linq;
using SportsStore.Domain.Entities; 
namespace SportsStore.Domain.Abstract {
public interface IProductRepository { IQueryable<Product> Products { get; } } }

This interface uses the IQueryable<T> interface to allow a sequence of Product objects to be obtained, without saying anything about how or where the data is stored or how it will be retrieved. A class that uses the IProductRepository interface can obtain Product objects without needing to know anything about where they are coming from or how they will be delivered. This is the essence of the repository pattern. We’ll revisit this interface throughout the development process to add features.

Making a Mock Repository

Now that we have defined an abstract interface, we could go ahead and implement the persistence mechanism and hook it up to a database. We are going to do that later in this chapter. In order to be able to start writing other parts of the application, we are going to create a mock implementation of the IProductRepository interface. We are going to do this in the AddBindings method of our NinjectControllerFactory class, as shown in Listing 7-5.

Listing 7-5. Adding the Mock IProductRepository Implementation
清单7-5. 添加IProductRepository的模仿实现

private void AddBindings() {
// Mock implementation of the IProductRepository Interface // IProductRepository接口的模仿实现 Mock<IProductRepository> mock = new Mock<IProductRepository>(); mock.Setup(m => m.Products).Returns(new List<Product> { new Product { Name = "Football", Price = 25 }, new Product { Name = "Surf board", Price = 179 }, new Product { Name = "Running shoes", Price = 95 } }.AsQueryable()); ninjectKernel.Bind<IProductRepository>().ToConstant(mock.Object); }

Visual Studio will be able to resolve the namespaces of all of the new types in these statements, but you’ll need to add a using statement to import the System.Linq namespace in order to get access to the AsQueryable extension method.
Visual Studio将能够解析这些语句中所有新类型的命名空间,但你需要添加一条using语句,引用System.Linq命名空间,以获得对AsQueryable扩展方法的访问。

Displaying a List of Products

We could spend the rest of this chapter building out the domain model and the repository, and not touch the UI project at all. We think you would find that boring, though, so we are going to switch tracks and start using the MVC Framework in earnest. We’ll add features to the model and the repository as we need them.

In this section, we are going to create a controller and an action method that can display details of the products in the repository. For the moment, this will be for only the data in the mock repository, but we’ll sort that out later. We’ll also set up an initial routing configuration, so that MVC knows how to map requests for the application to the controller we are going to create.

Adding a Controller

Right-click the Controllers folder in the SportsStore.WebUI project and select Add → Controller from the pop-up menus. Change the name of the controller to ProductController and ensure that the Template option is set to Empty controller. When Visual Studio opens the file for you to edit, you can remove the default action method that has been added automatically, so that your file looks like the one in Listing 7-6.
右击SportsStore.WebUI项目中的Controllers文件夹,并从弹出菜单中选择“添加” → “控制器”。将控制器的名称改为ProductController,并确保模板选项设置在“空控制器”上。当Visual Studio打开这个文件让你编辑时,你可以删除已经自动添加进来的默认的动作方法,因此,你的文件看上去如清单7-6。

Listing 7-6. The Empty ProductController Class
清单7-6. 空的ProductController类

using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract; 
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller { private IProductRepository repository;
public ProductController(IProductRepository productRepository) { repository = productRepository; } } }

You can see that we’ve added a constructor that takes an IProductRepository parameter. This will allow Ninject to inject the dependency for the product repository when it instantiates the controller class. Next, we are going to add an action method, called List, which will render a view showing the complete list of products, as shown in Listing 7-7.

Listing 7-7. Adding an Action Method
清单7-7. 添加一个动作方法

using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract; 
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller { private IProductRepository repository;
public ProductController(IProductRepository productRepository) { repository = productRepository;
} public ViewResult List() { return View(repository.Products); } } }

As you may remember from Chapter 3, calling the View method like this (without specifying a view name) tells the framework to render the default view for the action method. By passing a List of Product objects to the View method, we are providing the framework with the data with which to populate the Model object in a strongly typed view.

Adding the View

Of course, now we need to add the default view for the List action method. Right-click the List method and select Add View from the pop-up menu. Name the view List and check the option that creates a strongly typed view, as shown in Figure 7-7.


Figure 7-7. Adding the List view
图7-7. 添加List视图

For the model class, enter IEnumerable<SportsStore.Domain.Entities.Product>. You will need to type this in; it won’t be available from the drop-down list, which doesn’t include enumerations of domain objects. We will use the default Razor layout later on to add a consistent appearance to our views, so check the option to use a layout but leave the text box empty, as we have done in the figure. Click the Add button to create the view.

Knowing that the model in the view contains an IEnumerable<Product> means we can create a list by using a foreach loop in Razor, as shown in Listing 7-8.

Listing 7-8. The List.cshtml View
清单7-8. List.cshtml视图

@model IEnumerable<SportsStore.Domain.Entities.Product> 
@{ ViewBag.Title = "Products"; }
@foreach (var p in Model) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }

We’ve changed the title of the page and created a simple list. Notice that we don’t need to use the Razor text or @: elements. This is because each of the content lines in the code body is either a Razor directive or starts with an HTML element.

Tip Notice that we converted the Price property to a string using the ToString("c") method, which renders numerical values as currency, according to the culture settings that are in effect on your server. For example, if the server is set up as en-US, then (1002.3).ToString("c") will return $1,002.30, but if the server is set to fr-FR, then the same method will return 1 002,30 €. You can change the culture setting for your server by adding a section to the Web.config <system.web> node like this: <globalization culture="fr-FR" uiCulture="fr-FR" />.
提示:注意,我们使用了ToString(“c”)方法,把Price属性转换成一个的字符串,它会根据你服务器的语言设置把数字值渲染成货币。例如,如果服务器的设置为en-US,那么,(1002.3)ToString(“c”)将返回$1,002.3,但如果服务设置为fr-FR,那么同一方法将返回1002, 30 €。你可以修改服务器的语言设置,只要把以下小节添加到Web.config的<system.web>节点即可:<globalization culture=”fr-FR” uiCulture=”fr-FR” />。

Setting the Default Route

All we need to do now is tell the MVC Framework that requests that arrive for the root of our site (http://mysite/) should be mapped to the List action method in the ProductController class. We do this by editing the statement in the RegisterRoutes method of Global.asax.cs, as shown in Listing 7-9.

Listing 7-9. Adding the Default Route
清单7-9. 添加默认路由

public static void RegisterRoutes(RouteCollection routes) {
        "Default", // Route name(路由名)
        "{controller}/{action}/{id}", // URL with parameters(带参数的URL)
        new { controller = "Product", action = "List", id = UrlParameter.Optional }

You can see the changes in bold—change Home to Product and Index to List, as shown in the listing. We’ll cover the ASP.NET routing feature in detail in Chapter 11. For now, it’s enough to know that this change directs requests for the default URL to the action method we defined.
你可以看到以黑体表示的修改 — 将Home改为Product,以及将Index改为List,如上述清单所示。我们将在第11章讨论ASP.NET路由特性的细节。现在,知道这种修改是把对默认URL的请求定向到我们所定义的动作方法,这就够了。

Tip Notice that we have set the value of the controller in Listing 7-9 to be Product and not ProductController, which is the name of the class. This is a compulsory ASP.NET MVC naming scheme, in which controller classes always end in Controller, and you omit this part of the name when referring to the class.
提示:注意,我们在清单7-9中所设置的controller的值是Product,而不是ProductController,ProductController是类名。这是一个强制实行的ASP.NET MVC命名模式,控制器类总是以Controller结尾,而在对这个类进行引用时,要忽略其名字中的Controller这一部分。

Running the Application

We have all the basics in place. We have a controller with an action method that is called when the default URL is requested. That action method relies on a mock implementation of our repository interface, which generates some simple test data. The test data is passed to the view that we associated with the action method, and the view creates a simple list of the details for each product. If you run the application, you can see the result, which we have shown in Figure 7-8.


Figure 7-8. Viewing the basic application functionality
图7-8. 察看应用程序基本功能

The pattern of development for this application is typical for the ASP.NET MVC Framework in general. We invest a relatively long period of time getting everything set up, and then the basic functionality of the application comes together very quickly.
本应用程序的开发模式是典型的ASP.NET MVC框架的大致开发模式。我们花了较长时间让所有事建立起来,然后快速地集合了应用程序的基本功能。

Preparing a Database

We can already display simple views that contain details of our products, but we are still displaying the test data that our mock IProductRepository returns. Before we can implement a real repository, we need to set up a database and populate it with some data.

We are going to use SQL Server as the database, and we will access the database using the Entity Framework (EF), which is the .NET ORM framework. An ORM framework lets us work with the tables, columns, and rows of a relational database using regular C# objects. We mentioned in Chapter 4 that LINQ can work with different sources of data, and one of these is the Entity Framework. You’ll see how this simplifies things in a little while.
我们打算用SQL Server作为数据库,而且我们将用Entity Framework(实体框架 — EF)来访问该数据库,EF是.NET的ORM(对象关系映射)框架。ORM框架让我们可以用规则的C#对象对一个关系数据库的表、列、行进行工作。我们在第4章提到过,LINQ可以与不同的数据源一起工作,其中之一就是Entity Framework。你一会儿就会看到它如何对事情进行简化。

This is another area where you can choose from a wide range of tools and technologies. Not only are there different relational databases available, but you can also work with object repositories, document stores, and some very esoteric alternatives. There are many ORM frameworks as well, each of which takes a slightly different approach—variations that may give you a better fit for your projects.
这是你可以从广泛的工具和技术中进行选择另一个领域。不仅有不同的关系数据库可用,而且你也可以与不同的对象存储库、文档存储、以及其它十分深奥的技术等进行工作。也有很多ORM框架,每一个都采取了稍有不同的方法 — 可能会为你提供一些更适合于项目的变化。

We are using the Entity Framework for a couple of reasons. The first is that it is simple and easy to get it up and working. The second is that the integration with LINQ is first rate, and we like using LINQ. The third reason is that it is actually pretty good. The earlier releases were a bit hit-and-miss, but the current versions are very elegant and feature-rich.
我们要使用Entity Framework出于一些理由。第一是它简单,而且易于建立和工作。第二是它与LINQ的集成是一流的,而我们喜欢使用LINQ。第三是它确实很好。早期的版本有点混乱,但当前版本十分雅致且特性丰富。

Creating the Database

The first step is to create the database, which we are going to do using the built-in database management tools included in Visual Studio. Open the Server Explorer window (Figure 7-9) by selecting the item of the same name from the View menu.
第一步是创建数据库,我们打算用Visual Studio内建的数据库管理工具。通过在“视图”菜单中选择“服务器资源管理器(Server Explorer)”来打开“服务器资源管理器”窗口(图7-9)。


Figure 7-9. The Server Explorer window
图7-9. 服务器资源管理器窗口

Right-click Data Connections and select Create New Database from the pop-up menu. Enter the name of your database server and set the name of the new database to SportStore. If you have installed SQL Server on your development machine, the server name will be .\SQLEXPRESS, as shown in Figure 7-10.
右击“数据连接(Data Connections)”并从弹出菜单中选择“创建新数据库(Create New Database)”。键入数据库服务器名,并把新数据库的名字设为SportsStore。如果在你的开发机器上已经安装了SQL Server,此服务器名将是.\SQLEXPRESS,如图7-10所示。


Figure 7-10. Creating a new database
图7-10. 创建一个新数据库

Click the OK button to create the database. The Server Explorer window will be updated to reflect the new addition.

Defining the Database Schema

We need only one table in our database, which we will use to store our Product data. Using Server Explorer, expand the database you just added so you can see the Table item and right-click it. Select Add New Table from the menu, as shown in Figure 7-11.
在我们的数据库中只需要一个表,用来存储Product数据。利用服务器资源管理器,展开这个你刚添加的数据库,于是你可以看到“表(Tables)”条目,右击它。并从弹出菜单中选择“添加新表(Add New Table)”,如图7-11所示。


Figure 7-11. Adding a new table
图7-11. 添加新表

A template for creating the table will open. Add the columns shown in Figure 7-12. For each of the columns, be sure to select the right data type and to uncheck the Allow Nulls options.
这会打开一个创建表的模板。添加如图7-12所示的列。对每个列,要确保选择了正确的数据类型,并取消了“允许为空(Allow Nulls)”复选框。


Figure 7-12. Creating the table columns
图7-12. 创建表列

Right-click the ProductID column and select Set Primary Key. This will add the small yellow key that you can see in Figure 7-12. Right-click the ProductID column again and select the Properties menu item. In the Properties window, set the value of the Identity Column property to ProductID.
右击ProductID列,并选择“设为主键(Set Primary Key)”。这会添加如图7-12中所看到的黄色小钥匙。再次右击ProductID列,并选择“属性(Properties)”菜单条目。在属性窗口中,把“标识列”属性的值设置为ProductID。

Tip Setting the Identity Column property means that SQL Server will generate a unique primary key value when we add data to this table. When using a database in a web application, it can be very difficult to generate unique primary keys because requests from users arrive concurrently. Enabling this feature means we can store new table rows and rely on SQL Server to sort out unique values for us.
提示:设置标识列属性意味着,在向该表添加数据时,SQL Server会生成一个唯一的主键值。当在一个web应用程序中使用一个数据库时,生成唯一主键可能是很困难的,因为用户的请求是并发出现的。启用这一特性意味着,我们可以存储新的表行数据,并依靠SQL Server为我们排出唯一值。

When you’ve entered all of the columns and changed the properties, press Control+S to save the new table. You will be prompted to enter a name for the table, as shown in Figure 7-13. Set the name to Products and click OK to create the table.
当你已经键入了所有列并修改了这些属性时,按Ctrl + S来保存这个新表。这将提示你输入这个表的名字,如图7-13所示。将此名设为Products,点击“确定(OK)”以创建此表。


Figure 7-13. Namings the database table
图7-13. 命名数据库表

Adding Data to the Database

We are going to manually add some data to the database so that we have something to work with until we add the catalog administration features in Chapter 9. In the Solution Explorer window, expand the Tables item of the SportsStore database, right-click the Products table, and select Show Table Data. Enter the data shown in Figure 7-14. You can move from row to row by using the Tab key.
我们打算手工地将一些数据添加到该数据库,以使我们在第9章添加目录管理特性之前,有一些与之工作的东西。在“解决方案资源管理器”窗口中(应当是“服务器资源管理器”窗口 — 译者注),展开SportsStore数据库的“表”条目,右击Products表,选择“显示表数据(Show Table Data)”。键入如图7-14所示的数据。你可以用Tab键从一行移到另一行。

Note You must leave the ProductID column empty. It is an identity column so SQL Server will generate a unique value when you tab to the next row.
注:你必须让ProductID列为空。它是一个标识列,因此,当你跳到一下行时,SQL Server将生成一个唯一的值。


Figure 7-14. Adding data to the Products table
图7-14. 对Products表添加数据

Creating the Entity Framework Context

Version 4.1 of the Entity Framework includes a nice feature called code-first. The idea is that we can define the classes in our model and then generate a database from those classes.
Entity Framework 4.1版包含了一个叫做code-first(代码先行)的很好的特性。其思想是我们可以先定义模型中的类,然后再通过这些类生成一个数据库。

This is great for greenfield development projects, but these are few and far between. Instead, we are going to show you a variation on code-first, where we associate our model classes with an existing database. The first step is to add Entity Framework version 4.1 to our SportsStore.Domain project. The MVC 3 Tools Update that we installed in Chapter 2 automatically installs Entity Framework 4.1 on MVC Framework projects, but we need to do it manually for class library projects.
这很适合于绿地(greenfield)开发项目,但这些项目并不多见。因此,我们打算给你演示代码先行的一种变异,以此把模型类与现有的数据库关联在一起。第一步是把Entity Framework 4.1添加到我们的SportsStore.Domain项目。在第2章安装的“MVC 3工具更新(MVC 3 Tools Update)”自动地在“MVC框架项目”上安装了Entity Framework 4.1,但我们需要为“类库项目”手工地安装它(意即,对于SportsStore.WebUI项目(MVC框架项目),EF是被自动安装的,而对于SportsStore.Domain项目(类库项目),需要手工安装EF — 译者注)。

Right-click References and select Add Library Package Reference from the pop-up menu. Search or scroll down the list until you find the EntityFramework package, as shown in Figure 7-15, and then click the Install button. Visual Studio will download and install the latest Entity Framework version.
右击“引用”并从弹出菜单中选择“添加库包引用”。搜索或滚动列表,直到你找到EntityFramework包,如图7-15所示,然后点击“Install”按钮。Visual Studio将下载并安装最新版的Entity Framework。


Figure 7-15. Adding the EntityFramework library package
图7-15. 添加Entity Framework库包

The next step is to create a context class that will associate our simple model with the database. Add a new class called EFDbContext in the Concrete folder, and then edit the contents so that they match Listing 7-10.
下一步是创建一个将我们的简单模型与数据库关联的上下文类(context class)。在Concrete文件夹中添加一个名为EFDbContext的新类,然后编辑其内容使其如清单7-10。

Listing 7-10. The EFDbContext Class
清单7-10. EFDbContext类

public class EFDbContext : DbContext {
    public DbSet<Product> Products { get; set; }

To take advantage of the code-first feature, we need to create a class that is derived from System.Data.Entity.DbContext. This class then defines a property for each table that we want to work with. The name of the property specifies the table, and the type parameter of the DbSet result specifies the model that the Entity Framework should use to represent rows in that table. In our case, the property name is Products and the type parameter is Product. We want the Product model type to be used to represent rows in the Products table.
为了利用代码先行特性,我们需要创建一个派生于System.Data.Entity.DbContext的类。这个类然后为每个我们要与之工作的表定义一个属性。属性名指定为表名,并把DbSet结果的类型参数指定为实体框架应该用来表示表行的模型。在我们的例子中,该属性名是Products(数据库中的表名 — 译者注),而参数类型是Product(用来表示该表中一行数据的模型类(Product类)的类型(Product) — 译者注)。意即我们希望用Product模型类型来表示Products表的各个行。

We need to tell the Entity Framework how to connect to the database, and we do that by adding a database connection string to the Web.config file in the SportsStore.WebUI project with the same name as the context class, as shown in Listing 7-11.
我们需要告诉Entity Framework如何连接到数据库,要完成此事,只要在SportsStore.WebUI项目的Web.config文件中以这个上下文类同样的名字添加一条数据库连接字串即可,如清单7-11所示。

Listing 7-11. Adding a Database Connection
清单7-11. 添加一条数据库连接

        <add name="EFDbContext" connectionString="Data Source=TITAN\SQLEXPRESS;Initial
                Catalog=SportsStore;Persist Security Info=True;User ID=adam;Password=adam"

This connection string connects to TITAN, which is our database server. If you have installed SQL Server Express on your local machine, then the connection will be as shown in Listing 7-12.
这个连接字串连接到TITAN,这是我们的数据库服务器(指作者的数据库服务器 — 译者注)。如果你已经在本地机器上安装了SQL Server Express,那么这个连接可以如清单7-12所示。

Listing 7-12. Connecting to a Local SQL Server Express Installation
清单7-12. 连接到一个本地SQL Server Express

        <add name=”EFDbContext”
              connectionString=”Data Source=.\SQLEXPRESS;Initial Catalog=SportsStore; Integrated Security=SSPI” providerName=”System.Data.SqlClient”/>

It is important that the value of the name attribute in the connection string matches the name of the context class, because this is how the Entity Framework finds the database that we want to work with.
重要的是这个连接字串中name属性的值要与这个上下文类的名字匹配,因为这样Entity Framework才会找到我们想要与之工作的数据库。

Creating the Product Repository

We now have everything we need to implement the IProductRepository class for real. Add a class to the Concrete folder of the SportsStore.Domain project called EFProductRepository. Edit your class file so it matches Listing 7-13.现在,我们已经做好了真正实现IProductRepository类所需要的各种准备。把一个类添加到SportsStore.Domain项目的Concrete文件夹,取名为EFProductRepository。编辑这个类文件使之如清单7-13。

Listing 7-13. EFProductRepostory.cs
清单7-13. EFProductRepostory.cs

using System.Linq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities; 
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository { private EFDbContext context = new EFDbContext();
public IQueryable<Product> Products { get { return context.Products; } } } }

This is our repository class. It implements the IProductRepository interface and uses an instance of EFDbContext to retrieve data from the database using the Entity Framework. You’ll see how we work with the Entity Framework (and how simple it is) as we add features to the repository.
这是我们的存储库类。它实现了IProductRepository接口,并使用了一个EFDbContext实例,以使用Entity Framework来接收数据库的数据。当你向存储库添加特性时,你将会看到,我们是如何与Entity Framework架进行工作的(而且它是多么简单)。

The last stage is to replace the Ninject binding for our mock repository with a binding for our real one. Edit the NinjectControllerFactory class in the SportsStore.WebUI project so that the AddBindings method looks like Listing 7-14.

Listing 7-14. Adding the Real Repository Binding
清单7-14. 添加实际存储库的绑定

private void AddBindings() {
    // put additional bindings here
    // 这里放置附加绑定

The new binding is shown in bold. It tells Ninject that we want to create instances of the EFProductRepository class to service requests for the IProductRepository interface. All that remains now is to run the application again. The results are shown in Figure 7-16, and you can see that our list now contains the product data we put into the database.


Figure 7-16. The result of implementing the real repository
图7-16. 实现实际存储库的结果

Adding Pagination

You can see from Figure 7-16 that all of the products in the database are displayed on a single page. In this section, we will add support for pagination so that we display a number of products on a page, and the user can move from page to page to view the overall catalog. To do this, we are going to add a parameter to the List method in the Product controller, as shown in Listing 7-15.

Listing 7-15. Adding Pagination Support to the Product Controller List Method
清单7-15. 对Product控制器的List方法添加分页支持

using System.Linq;
using System.Web.Mvc;
using SportsStore.Domain.Abstract; 
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
public int PageSize = 4; // We will change this later(稍后会对此进行修改) private IProductRepository repository;
public ProductController(IProductRepository repoParam) { repository = repoParam; }
public ViewResult List(int page = 1) { return View(repository.Products .OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize)); } } }

The additions to the controller class are shown in bold. The PageSize field specifies that we want four products per page. We’ll come back and replace this with a better mechanism later on. We have added an optional parameter to the List method. This means that if we call the method without a parameter (List()), our call is treated as though we had supplied the value we specified in the parameter definition (List(1)). The effect of this is that we get the first page when we don’t specify a page value. LINQ makes pagination very simple. In the List method, we get the Product objects from the repository, order them by the primary key, skip over the products that occur before the start of our page, and then take the number of products specified by the PageSize field.


We can unit test the pagination feature by creating a mock repository, injecting it into the constructor of the ProductController class, and then calling the List method to request a specific page. We can then compare the Product objects we get with what we would expect from the test data in the mock implementation. See Chapter 6 for details of how to set up unit tests. Here is the unit test we created for this purpose:

public void Can_Paginate() {
    // 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"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
        new Product {ProductID = 4, Name = "P4"},
        new Product {ProductID = 5, Name = "P5"}
// create a controller and make the page size 3 items // 创建一个控制器,并使页面大小为3条数据项 ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;
// Action // 动作 IEnumerable<Product> result = (IEnumerable<Product>)controller.List(2).Model;
// Assert // 断言 Product[] prodArray = result.ToArray(); Assert.IsTrue(prodArray.Length == 2); Assert.AreEqual(prodArray[0].Name, "P4"); Assert.AreEqual(prodArray[1].Name, "P5"); }

Notice how easy it is to get the data that is returned from a controller method. We call the Model property on the result to get the IEnumerable<Product> sequence that we generated in the List method. We can then check that the data is what we want. In this case, we converted the sequence to an array, and checked the length and the values of the individual objects.

Displaying Page Links

If you run the application, you’ll see that there are only four items shown on the page. If you want to view another page, you can append query string parameters to the end of the URL, like this:


You will need to change the port part of the URL to match whatever port your ASP.NET development server is running on. Using these query strings, we can navigate our way through the catalog of products.

Of course, only we know this. There is no way for customers to figure out that these query string parameters can be used, and even if there were, we can be pretty sure that customers aren’t going to want to navigate this way. We need to render some page links at the bottom of the each list of products so that customers can navigate between pages. To do this, we are going to implement a reusable HTML helper method, similar to the Html.TextBoxFor and Html.BeginForm methods we used in Chapter 3. Our helper will generate the HTML markup for the navigation links we need.

Adding the View Model

To support the HTML helper, we are going to pass information to the view about the number of pages available, the current page, and the total number of products in the repository. The easiest way to do this is to create a view model, which we mentioned briefly in Chapter 4. Add the class shown in Listing 7-16, called PagingInfo, to the Models folder in the SportsStore.WebUI project.

Listing 7-16. The PagingInfo View Model Class
清单7-16. PagingInfo视图模型类

using System; 
namespace SportsStore.WebUI.Models {
public class PagingInfo { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int CurrentPage { get; set; }
public int TotalPages { get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); } } } }

A view model isn’t part of our domain model. It is just a convenient class for passing data between the controller and the view. To emphasize this, we have put this class in the SportsStore.WebUI project to keep it separate from the domain model classes.

Adding the HTML Helper Method

Now that we have the view model, we can implement the HTML helper method, which we are going to call PageLinks. Create a new folder in the SportsStore.WebUI project called HtmlHelpers and add a new static class called PagingHelpers. The contents of the class file are shown in Listing 7-17.

Listing 7-17. The PagingHelpers Class
清单7-17. PagingHelpers类

using System;
using System.Text;
using System.Web.Mvc;
using SportsStore.WebUI.Models; 
namespace SportsStore.WebUI.HtmlHelpers {
public static class PagingHelpers {
public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pagingInfo, Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder(); for (int i = 1; i <= pagingInfo.TotalPages; i++) { TagBuilder tag = new TagBuilder("a"); // Construct an <a> tag(构造一个<a>标签) tag.MergeAttribute("href", pageUrl(i)); tag.InnerHtml = i.ToString(); if (i == pagingInfo.CurrentPage) tag.AddCssClass("selected"); result.Append(tag.ToString()); }
return MvcHtmlString.Create(result.ToString()); } } }

The PageLinks extension method generates the HTML for a set of page links using the information provided in a PagingInfo object. The Func parameters provides the ability to pass in a delegate that will be used to generate the links to view other pages.


To test the PageLinks helper method, we call the method with test data and compare the results to our expected HTML. The unit test method is as follows:

public void Can_Generate_Page_Links() {
// Arrange - define an HTML helper - we need to do this // in order to apply the extension method // 布置 — 定义一个HTML辅助器 — 为了这个运用扩展方法, // 我们需要这么做 HtmlHelper myHelper = null;
// Arrange - create PagingInfo data // 布置 — 创建PagingInfo数据 PagingInfo pagingInfo = new PagingInfo { CurrentPage = 2, TotalItems = 28, ItemsPerPage = 10 };
// Arrange - set up the delegate using a lambda expression // 布置 — 用一个lambda表达式来建立委托 Func<int, string> pageUrlDelegate = i => "Page" + i;
// Act // 动作 MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
// Assert // 断言 Assert.AreEqual(result.ToString(), @"<a href=""Page1"">1</a><a class=""selected"" href=""Page2"">2</a><a href=""Page3"">3</a>"); }

This test verifies the helper method output by using a literal string value that contains double quotes. C# is perfectly capable of working with such strings, as long as we remember to prefix the string with @ and use two sets of double quotes ("") in place of one set of double quotes. We must also remember not to break the literal string into separate lines, unless the string we are comparing to is similarly broken. For example, the literal we use in the test method has wrapped onto two lines because the width of a printed page is narrow. We have not added a newline character; if we did, the test would fail.

Remember that an extension method is available for use only when the namespace that contains it is in scope. In a code file, this is done with a using statement, but for a Razor view, we must add a configuration entry to the Web.config file, or add an @using statement to the view itself. There are, confusingly, two Web.config files in a Razor MVC project: the main one, which resides in the root directory of the application project, and the view-specific one, which is in the Views folder. The change we need to make is to the Views/Web.config file and is shown in Listing 7-18.

Listing 7-18. Adding the HTML Helper Method Namespace to the Views/Web.config File
清单7-18. 将HTML辅助器方法的命名空间添加到View/Web.config文件

    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory,
            System.Web.Mvc, Version=,
            Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
            <add namespace="System.Web.Mvc" />
            <add namespace="System.Web.Mvc.Ajax" />
            <add namespace="System.Web.Mvc.Html" />
            <add namespace="System.Web.Routing" />
            <add namespace="SportsStore.WebUI.HtmlHelpers"/>

Every namespace that we need to refer to in a Razor view needs to be declared either in this way or in the view itself with an @using statement.

Adding the View Model Data

We are not quite ready to use our HTML helper method. We have yet to provide an instance of the PagingInfo view model class to the view. We could do this using the View Data or View Bag features, but we would need to deal with casting to the appropriate type.
我们还没有做好使用HTML辅助器方法的准备。我们还要把这个PagingInfo视图模型类的一个实例提供给视图。我们可以用View Data(视图数据)或View Bag(视图包)特性来做这件事,但我们需要进行处理,把它转换成相应类型。

We would rather wrap all of the data we are going to send from the controller to the view in a single view model class. To do this, add a new class called ProductsListViewModel to the Models folder of the SportsStore.WebUI folder. The contents of this class are shown in Listing 7-19.

Listing 7-19. The ProductsListViewModel View Model
清单7-19. 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; } } }

We can now update the List method in the ProductController class to use the ProductsListViewModel class to provide the view with details of the products to display on the page and details of the pagination, as shown in Listing 7-20.

Listing 7-20. Updating the List Method
清单7-20. 更新List方法

public ViewResult List(int page = 1) {
ProductsListViewModel viewModel = new ProductsListViewModel { Products = repository.Products .OrderBy(p => p.ProductID) .Skip((page - 1) * PageSize) .Take(PageSize), PagingInfo = new PagingInfo { CurrentPage = page, ItemsPerPage = PageSize, TotalItems = repository.Products.Count() } }; return View(viewModel); }

These changes pass a ProductsListViewModel object as the model data to the view.


We need to ensure that the correct pagination data is being sent by the controller to the view. Here is the unit test we have added to our test project to address this:

public void Can_Send_Pagination_View_Model() {
    // 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"},
        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 - create a controller and make the page size 3 items // 布置 — 创建一个控制器,并使页面大小为3条数据项 ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;
// Action // 动作 ProductsListViewModel result = (ProductsListViewModel)controller.List(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); }

We also need to modify our earlier pagination unit test, contained in the Can_Paginate method. It relies on the List action method returning a ViewResult whose Model property is a sequence of Product objects, but we have wrapped that data inside another view model type. Here is the revised test:

public void Can_Paginate() {
    // 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"},
        new Product {ProductID = 2, Name = "P2"},
        new Product {ProductID = 3, Name = "P3"},
        new Product {ProductID = 4, Name = "P4"},
        new Product {ProductID = 5, Name = "P5"}
// create a controller and make the page size 3 items // 创建控制器,并使页面大小为3条数据项 ProductController controller = new ProductController(mock.Object); controller.PageSize = 3;
// Action // 动作 ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
// Assert // 断言 Product[] prodArray = result.Products.ToArray(); Assert.IsTrue(prodArray.Length == 2); Assert.AreEqual(prodArray[0].Name, "P4"); Assert.AreEqual(prodArray[1].Name, "P5"); }

We would usually create a common setup method, given the degree of duplication between these two test methods. However, since we are delivering the unit tests in individual sidebars like this one, we are going to keep everything separate, so you can see each test on its own.
我们通常会创建一个公用的设置方法,在其中给出这两个测试方法之间的代码重复部分(注,这短短的一句话,为我们进行单元测试提供了一种思路,即,把各测试单元所需要的数据放到一个公用方法中进行设置,这样可以免去各个单元测试中都要进行数据设置的重复劳动 — 译者注)。然而,由于我们是以像上例这样的个别工具的形式来交付单元测试的,所以我们仍打算保持所有事情独立,因此,你看到的仍是各个单独的测试。

At the moment, the view is expecting a sequence of Product objects, so we need to update List.cshtml, as shown in Listing 7-21, to deal with the new view model type.

Listing 7-21. Updating the List.cshtml View
清单7-21. 更新List.cshtml视图

@model SportsStore.WebUI.Models.ProductsListViewModel
@{ ViewBag.Title = "Products"; }
@foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }

We have changed the @model directive to tell Razor that we are now working with a different data type. We also needed to update the foreach loop so that the data source is the Products property of the model data.

Displaying the Page Links

We have everything in place to add the page links to the List view. We have created the view model that contains the paging information, updated the controller so that this information is passed to the view, and changed the @model directive to match the new model view type. All that remains is to call our HTML helper method from the view, which you can see in Listing 7-22.

Listing 7-22. Calling the HTML Helper Method
清单7-22. 调用HTML辅助器方法

@model SportsStore.WebUI.Models.ProductsListViewModel
@{ ViewBag.Title = "Products"; }
@foreach (var p in Model.Products) { <div class="item"> <h3>@p.Name</h3> @p.Description <h4>@p.Price.ToString("c")</h4> </div> }
<div class="pager"> @Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x})) </div>

If you run the application, you’ll see that we’ve added page links, as illustrated in Figure 7-17. The style is still pretty basic, and we’ll fix that later in the chapter. What’s important at the moment is that the links take us from page to page in the catalog and let us explore the products for sale.


Figure 7-17. Displaying page navigation links
图7-17. 显示页面导航链接


If you’ve worked with ASP.NET before, you might think that was a lot of work for a pretty unimpressive result. It has taken us pages and pages just to get a page list. If we were using Web Forms, we could have done the same thing using the ASP.NET Web Forms GridView control, right out of the box, by hooking it up directly to our Products database table.
如果你以前曾用过ASP.NET,你也许会认为,为了一个不起眼的结果做了太多的工作。花了这么多篇幅只是得到了一个页面的列表。如果我们使用Web表单,可以用ASP.NET Web表单的GridView控件,直接把它挂接到Products数据库表,就可以做同样的事情了。

What we have accomplished so far doesn’t look like much, but it is very different from dragging a GridView onto a design surface. First, we are building an application with a sound and maintainable architecture that involves proper separation of concerns. Unlike the simplest use of GridView, we have not directly coupled the UI and the database together—an approach that gives quick results but that causes pain and misery over time. Second, we have been creating unit tests as we go, and these allow us to validate the behavior of our application in a natural way that’s nearly impossible with a Web Forms GridView control.
到目前为止我们所完成的看上去并不太多,但它与把一个GridView拖拽到一个设计界面完全不同。首先,我们是在建立一个彻底可维护的、包含了恰当关注分离体系结构的应用程序。与GridView最简单的使用不同,我们没有把UI和数据库直接耦合在一起 — 耦合可以快速得到结果,但会长期痛苦。其次,随着事情的进行,我们一直在创建单元测试,这让我们能够以自然的方式去检验应用程序的行为,这对Web表单的GridView控件几乎是不可能的。

Finally, bear in mind that a lot of this chapter has been given over to creating the underlying infrastructure on which the application is built. We need to define and implement the repository only once, for example, and now that we have, we’ll be able to build and test new features quickly and easily, as the following chapters will demonstrate.

Improving the URLs

We have the page links working, but they still use the query string to pass page information to the server, like this:


We can do better, specifically by creating a scheme that follows the pattern of composable URLs. A composable URL is one that makes sense to the user, like this one:


Fortunately, MVC makes it very easy to change the URL scheme because it uses the ASP.NET routing feature. All we need to do is add a new route to the RegisterRoutes method in Global.asax.cs, as shown in Listing 7-23.

Listing 7-23. Adding a New Route
清单7-23. 添加一条新路由

public static void RegisterRoutes(RouteCollection routes) {
routes.MapRoute( null, // we don't need to specify a name(不需要指定名称) "Page{page}", new { Controller = "Product", action = "List" } );
routes.MapRoute( "Default", // Route name(路由名) "{controller}/{action}/{id}", // URL with parameters(带参数的URL) new { controller = "Product", action = "List", id = UrlParameter.Optional } ); }

It is important that you add this route before the Default one. As you’ll see in Chapter 11, routes are processed in the order they are listed, and we need our new route to take precedence over the existing one.

This is the only alteration we need to make to change the URL scheme for our product pagination. The MVC Framework is tightly integrated with the routing function, and so a change like this is automatically reflected in the result produced by the Url.Action method (which is what we use in the List.cshtml view to generate our page links). Don’t worry if routing doesn’t make sense to you at the moment—we’ll explain it in detail in Chapter 11. If you run the application and navigate to a page, you’ll see the new URL scheme in action, as illustrated in Figure 7-18.
这是唯一需要对产品分页的URL方案进行修改的地方。MVC框架与路由功能是密切集成的,因此这样的修改将自动地反映在Url.Action方法(这是我们在List.cshtml视图中用来生成页面链接所使用的方法)的处理结果中。如果你此时对路由还不熟悉,不用着急 — 我们将在第11章详细解释它。如果你运行这个应用程序,并导航到一个页面,你将看到这个新的URL方案在起作用。如图7-18所示。


Figure 7-18. The new URL scheme displayed in the browser
图7-18. 显示在浏览器中的新URL方案

Styling the Content

We’ve built a great deal of infrastructure, and our application is really starting to come together, but we have not paid any attention to its appearance. Even though this book isn’t about web design or CSS, the SportStore application design is so miserably plain that it undermines its technical strengths. In this section, we’ll put some of that right.

Note In this part of the chapter, we will ask you to add CSS styles without explaining their meaning. If you want to learn more about CSS, we recommend Pro CSS and HTML Design Patterns by Michael Bowers (Apress, 2007) and Beginning HTML with CSS and HTML by David Schultz and Craig Cook (Apress, 2007).
注:本章的这一部分,我们将要求你添加CSS样式而不解释它们的意义。如果你想学习更多关于CSS的内容,我们推荐Pro CSS and HTML Design Patterns(《精通HTML与CSS设计模式》),Michael Bowers著,Apress 2007年出版。

We are going to implement a classic two-column layout with a header, as shown in Figure 7-19.


Figure 7-19. The design goal for the SportsStore application
图7-19. SportsStore应用程序的设计目标

Defining Common Content in the Layout

The Razor layout system is the equivalent of the ASPX master page system. We can define content in one place, and then selectively apply it to individual views to create a consistent appearance in our application. We explained how Razor layouts work and are applied in Chapter 5. When we created the List.cshtml view for the Product controller, we asked you to check the option to use a layout, but leave the box that specifies a layout blank. This has the effect of using the default layout, _Layout.cshtml, which can be found in the Views/Shared folder of the SportsStore.WebUI project. Open this file and apply the changes shown in Listing 7-24.

Listing 7-24. Modifying the Default Razor Layout
清单7-24. 修改默认的Razor布局

<!DOCTYPE html>
    <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>
<body> <div id="header"> <div class="title">SPORTS STORE(体育用品商店)</div> </div> <div id="categories"> Will put something useful here later(稍后将在此放置一些有用的东西) </div> <div id="content"> @RenderBody() </div> </body> </html>

Adding CSS Rules

The HTML markup in Listing 7-24 is characteristic of an ASP.NET MVC application. It is simple and purely semantic. It describes the content, but says nothing about how it should be laid out on the screen. We will use CSS to tell the browser how the elements we just added should be laid out.
清单7-24中的HTML标记是一个ASP.NET MVC应用程序的特征。它简单而且是纯语义的。它描述了内容,但对它如何布置在屏幕上什么也没做。我们将用CSS来告诉浏览器应当如何显示刚添加的元素。

Visual Studio creates a CSS file for us automatically, even when creating an empty project. This Site.css file can be found in the Content folder of the SportsStore.WebUI project. This file is already referenced in the _Layout.cshtml file, as follows:
Visual Studio为我们自动创建了一个CSS文件,即使创建一个空项目时也会创建它。此即为在SportsStore.WebUI项目的Content文件夹中可以找到的Site.css文件。在_Layout.cshtml文件中引用了这个文件,如下:

<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />

Tip Notice that the CSS and JavaScript files that are referenced in Listing 7-24 are done so using the @Url.Content method. Unlike the ASPX view engine, Razor doesn’t automatically interpret the tilde character (~) as a reference for the root of the application, so we must do this explicitly using the helper method.

Open the Site.css file and add the styles shown in Listing 7-25 to the bottom of the file (don’t remove the existing content in Site.css). You don’t need to type these in by hand. You can download the CSS additions and the rest of the project as part of the code samples that accompany this book.

Listing 7-25. Defining CSS
清单7-25. 定义CSS

BODY { font-family: Cambria, Georgia, "Times New Roman"; margin: 0; }
DIV#header DIV.title, DIV.item H3, DIV.item H4, DIV.pager A {
  font: bold 1em "Arial Narrow", "Franklin Gothic Medium", Arial;
DIV#header { background-color: #444; border-bottom: 2px solid #111; color: White; }
DIV#header DIV.title { font-size: 2em; padding: .6em; }
DIV#content { border-left: 2px solid gray; margin-left: 9em; padding: 1em; }
DIV#categories { float: left; width: 8em; padding: .3em; }
DIV.item { border-top: 1px dotted gray; padding-top: .7em; margin-bottom: .7em; } DIV.item:first-child { border-top:none; padding-top: 0; } DIV.item H3 { font-size: 1.3em; margin: 0 0 .25em 0; } DIV.item H4 { font-size: 1.1em; margin:.4em 0 0 0; } DIV.pager { text-align:right; border-top: 2px solid silver; padding: .5em 0 0 0; margin-top: 1em; } DIV.pager A { font-size: 1.1em; color: #666; text-decoration: none; padding: 0 .4em 0 .4em; } DIV.pager A:hover { background-color: Silver; } DIV.pager A.selected { background-color: #353535; color: White; }

注:根据W3C的HTML以及CSS等标准规范,HTML文档中的标签、以及CSS样式表中的HTML对象均应当用小写字母进行标记。上述CSS样式设置应当说是不太规范的,比如:DIV应当用div,H3应当用h3等。目前,HTML及CSS虽然仍是大小写兼容的,但未来就不好说了,希望读者不要养成使用大写字母的习惯。 — 译者注

If you run the application, you’ll see that we have improved the appearance—at least a little, anyway. The changes are shown in Figure 7-20.
如果你运行应用程序,你将看到我们已经改善了其外观 — 至少改善了一点。其变化如图7-20所示。


Figure 7-20. The design-enhanced SportsStore application
图7-20. 增强设计的SportsStore应用程序

Creating a Partial View

As a finishing trick for this chapter, we are going to refactor the application to simplify the List.cshtml view. We are going to create a partial view, which is a fragment of content that is embedded in another view. Partial views are contained within their own files and are reusable across views, which can help reduce duplication, especially if you need to render the same kind of data in several places in your application.

To add the partial view, right-click the /Views/Shared folder in the SportsStore.WebUI project and select Add → View from the pop-up menu. Set the name of the view to ProductSummary. We want to display details of a product, so select the Product class from the Model class drop-down menu or type in the qualified class name by hand. Check the Create as a partial view option, as shown in Figure 7-21.
为了添加分部视图,右击SportsStore.WebUI项目中的/Views/Shared文件夹,然后从弹出菜单中选择“添加” → “视图”。将视图命名为ProductSummary。我们希望显示一个产品的细节,因此从“模型类”下拉列表框中选择Product类,或手工输入可用的类名。选中“创建为分部视图(Create as a partial view)”复选框,如图7-21所示。


Figure 7-21. Creating a partial view
图7-21. 创建分部视图

Click the Add button, and Visual Studio will create a partial view file at Views/Shared/ProductSummary.cshtml. A partial view is very similar to a regular view, except that when it is rendered, it produces a fragment of HTML, rather than a full HTML document. If you open the ProductSummary view, you’ll see that it contains only the model view directive, which is set to our Product domain model class. Apply the changes shown in Listing 7-26.
点击“添加”按钮,Visual Studio将创建一个分部视图文件/Views/Shared/ProductSummary.cshtml。分部视图与常规视图十分相似,只是它被渲染时产生的是一个HTML片段,而不是整个HTML文档。如果你打开这个ProductSummary视图,你将看到它只包含model视图指示符,它被设置为我们的Product域模型类。运用如清单7-26所示的修改。

Listing 7-26. Adding Markup to the ProductSummary Partial View
清单7-26. 将标记添加到ProductSummary分部视图

@model SportsStore.Domain.Entities.Product
<div class="item"> <h3>@Model.Name</h3> @Model.Description <h4>@Model.Price.ToString("c")</h4> </div>

Now we need to update Views/Products/List.cshtml so that it uses the partial view. You can see the change in Listing 7-27.

Listing 7-27. Using a Partial View from List.cshtml
清单7-27. 在List.cshtml中使用分部视图

@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})) </div>

We’ve taken the markup that was previously in the foreach loop in the List.cshtml view and moved it to the new partial view. We call the partial view using the Html.RenderPartial helper method. The parameters are the name of the view and the view model object.

Tip The RenderPartial method doesn’t return HTML markup like most other helper methods. Instead, it writes content directly to the response stream, which is why we must call it like a complete line of C#, using a semicolon. This is slightly more efficient than buffering the rendered HTML from the partial view, since it will be written to the response stream anyway. If you prefer a more consistent syntax, you can use the Html.Partial method, which does exactly the same as the RenderPartial method, but returns an HTML fragment and can be used as @Html.Partial("ProductSummary", p).
提示:RenderPartial方法并不像大多数其它辅助器方法那样返回HTML标记。相反,它把内容直接写到响应流,这是我们必须用一个分号,像一个完整的C#程序行一样来调用它的原因。这比缓冲已渲染的分部视图的HTML更有效一些,因为它将被写到响应流。如果你喜欢一种更一致的语法,你可以用Html.partial方法,它完成与RenderPartial方法同样的事情,但返回一个HTML片段,并能够像@Html.Partial(“ProductSummary”, p)一样来使用。

Switching to a partial view like this is good practice, but it doesn’t change the appearance of the application. If you run it, you’ll see that the display remains as before, as shown in Figure 7-22.


Figure 7-22. Applying a partial view
图7-22. 运用分部视图


In this chapter, we have built most of the core infrastructure for the SportsStore application. It doesn’t have many features that you could demonstrate to a client at this point, but behind the scenes, we have the beginnings of a domain model, with a product repository that is backed by SQL Server and the Entity Framework. We have a single controller, ProductController, that can produce paginated lists of products, and we have set up DI and a clean and friendly URL scheme.
本章我们已经建立了SportsStore应用程序最核心的基础结构。它此刻并没有很多可以给客户端演示的特性,但在幕后,我们有了基本的域模型,它带有一个产品存储库,其背后有SQL Server和Entity Framework的支持。我们还有了一个控制器,ProductController,它可以产生分页的产品列表,并且我们已经建立了DI和一个清晰友好的URL方案。

If this chapter felt like a lot of setup for little benefit, then the next chapter will balance the equation. Now that we have the fundamental elements out of the way, we can forge ahead and add all of the customer-facing features: navigation by category, a shopping cart, and a checkout process.

posted @ 2012-06-15 17:37  r01cn  阅读(6827)  评论(5编辑  收藏  举报