# SportsStore—A Real Application SportsStore — 一个真实的应用程序

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.

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.

UNIT TESTING 单元测试

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.

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.

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.

Figure 7-1. Creating a blank solution

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

Project Name

Visual Studio Project Template
Visual Studio项目模板
Purpose

SportsStore.Domain C# Class Library
C#类库
Holds the domain entities and logic; set up for persistence via a repository created with the 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.

Figure 7-2. Creating the unit test project

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将生成两个我们用不到的你可以删除的文件：SportsSDtore.Domain项目中的Class1.cs文件，和SportsStore.UnitTests项目中的UnitTest1.cs类。当你完成上述工作之后，你的解决方案浏览器窗口应该看上去如图7-3所示。

Figure 7-3. The projects shown in the Solution Explorer window

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.

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.

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

（上面只是命令示范，真正要添加的引用应当如表7-2所示 — 译者注）

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

Project Name

Tool Dependencies

Project Dependencies 项目依赖性
SportsStore.Domain None None
SportsStore.WebUI Ninject SportsStore.Domain
SportsStore.UnitTests Ninject
Moq
SportsStore.Domain
SportsStore.WebUI

（注意，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 建立DI容器

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.

 n 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的类型或命名空间"，这显然是需要你添加一条using语句把SportsStore.Domain.Entities命名空间纳入到你的类的范围中来。做这件事最好的办法是把光标定位到引起错误的这个类型上，按Ctrl + .（点）。Visual Studio将猜出需要哪个命名空间，并自动地弹出让你添加using语句的菜单。如果需要你添加对一个程序集的引用以找到一个类型时，我们会给出明确的说明。
Listing 7-1. The NinjectControllerFactory Class
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();
}
protected override IController GetControllerInstance(RequestContext requestContext,
Type controllerType) {
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
}
}
}

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
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);

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

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.

### 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.

Figure 7-5. Starting the application without using the debugger

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.

## 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. 我们打算从域模型开始。MVC应用程序中有太多的事都是围绕域模型，因此，这是开始工作最好的地方。

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

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

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.

### 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.

Create a new top-level folder inside the SportsStore.Domain project called Abstract and a new interface called IProductsRepository, 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.

Listing 7-4. The IProductRepository Interface File

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

// Mock implementation of the IProductRepository Interface
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.

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.

Listing 7-6. The Empty ProductController Class

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

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.

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

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

@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.

 n 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 node like this: . 提示：注意，我们把Price属性转换成了一个使用了ToString("c")方法的字符串，它会根据你服务器的语言设置把数字值渲染成货币。例如，如果服务的设置为en-US，那么，(1002.3)ToString("c")将返回$1,002.3，但如果服务设置为fr-FR，那么同一方法将返回1002, 30 €。你可以修改服务器的语言设置，只要把以下小节添加到Web.config的节点即可：

### 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

public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
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.

 n 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中所设置的控制器的值已经是Product而不是ProductController，这是类的名字。这是一个强制实行的ASP.NET MVC命名模式，控制器类总是以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

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.

## 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.

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.

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.

### 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.

Figure 7-9. The Server Explorer window

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.

Figure 7-10. Creating a new database

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.

Figure 7-11. Adding a new table

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.

Figure 7-12. Creating the table columns

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.

 n 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.

Figure 7-13. Namings the database table

### 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.

 n 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

### 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.

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.

Figure 7-15. Adding the EntityFramework library package

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.

Listing 7-10. The EfDbContext Class

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.

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.

Listing 7-11. Adding a Database Connection

<configuration>
<connectionStrings>
providerName="System.Data.SqlClient"/>
</connectionStrings>
...

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.

Listing 7-12. Connecting to a Local SQL Server Express Installation

<configuration>
<connectionStrings>
connectionString="Data Source=.\SQLEXPRESS;Initial Catalog=SportsStore; Integrated Security=SSPI" providerName="System.Data.SqlClient"/>
</connectionStrings>
...

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.

### Creating the Product Repository 生成Product存储库

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.

Listing 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.

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

ninjectKernel.Bind<IProductRepository>().To<EFProductRepository>();
}

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

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

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.

UNIT TEST: PAGINATION

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:

[TestMethod]
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"}
}.AsQueryable());
// create a controller and make the page size 3 items
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.

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:

http://localhost:23081/?page=2

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. 为了支持HTML辅助方法，我们打算把关于可用页面数以及存储库中产品总数等方面的信息传递给视图。做这件事最容易的办法是生成一个视图模型，这是我们在第4章概要提到的。把清单7-16所示的、名为PagingInfo的类添加到SportsStore.WebUI项目的Models文件夹。

Listing 7-16. The PagingInfo View Model Class

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 添加HTML辅助方法

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. 现在，我们有了视图模型，我们可以实现这个HTML辅助方法了，我们将之称为PageLinks。在SportsStore.WebUI项目中生成一个新文件夹，名为HtmlHelpers，并添加一个新的静态类，名为PagingHelpers。类文件的内容如清单7-17所示。

Listing 7-17. The PagingHelpers Class

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
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage)
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:

[TestMethod]
// Arrange - define an HTML helper - we need to do this
// in order to apply the extension method
HtmlHelper myHelper = null;
// Arrange - create PagingInfo data
PagingInfo pagingInfo = new PagingInfo {
CurrentPage = 2,
TotalItems = 28,
ItemsPerPage = 10
};
// Arrange - set up the delegate using a lambda expression
Func<int, string> pageUrlDelegate = i => "Page" + i;
// Act
// 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

<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory,
System.Web.Mvc, Version=3.0.0.0,
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
</namespaces>
...

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.

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

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

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.

UNIT TEST: PAGE MODEL VIEW DATA

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:

[TestMethod]
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"}
}.AsQueryable());
// Arrange - create a controller and make the page size 3 items
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:

[TestMethod]
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"}
}.AsQueryable());
// create a controller and make the page size 3 items
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

@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

@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.

WHY NOT JUST USE A GRIDVIEW?

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.

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.

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 改进URL

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

http://localhost/?page=2

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:

http://localhost/Page2

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

public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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
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.

Figure 7-18. The new URL scheme displayed in the browser

## 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.

 n 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

### 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.
Razor布局系统等同于ASPX母板页系统。我们可以在一个地方定义内容，然后有选择地把它运用于个别视图以生成应用程序一致的外观。我们在第5章中解释了Razor布局是如何工作以及如何运用的。当我们为Product控制器生成List.cshtml视图时，我们要求你打上了“使用一个布局”的复选框，但让那个下拉列表框保留为空。这便使用了默认布局效果，_Layout.cshtml，它可以在SportsStore.WebUI项目的Views/Shared文件夹中找到。打开这个文件并运用清单7-24所示的修改。

Listing 7-24. Modifying the Default Razor Layout

<!DOCTYPE html>
<html>
<title>@ViewBag.Title</title>
<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>

<body>
<div class="title">SPORTS STORE</div>
</div>
<div id="categories">
Will put something useful here later
</div>
<div id="content">
@RenderBody()
</div>
</body>
</html>

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.

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文件中作了引用，如下：

 n 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. 提示：注意，在清单7-24中引用的CSS和JavaScript文件已经用@Url.Content方法做了这事。与ASPX视图引擎不同，Razor并不自动地把波浪符（~）解析为应用程序的根，因此，我们必须用辅助方法明确地做好这件事。

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

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#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.

Figure 7-20. The design-enhanced SportsStore application

### 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.

Figure 7-21. Creating a partial view

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.

Listing 7-26. Adding Markup to the ProductSummary Partial View

@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

@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.

 n 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

## Summary 概要

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.