【譯著】第7章 SportsStore:一個真實的應用程序 — 《精通ASP.NET MVC 3框架》


C H A P T E R  7
■ ■ ■

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.
我們已經建立了一個快速簡單的MVC應用程序,也考察了MVC模式,還更新了關於C#基本特性、以及良好的MVC開發人員所需要的工具等方面的知識。現在,到了把所有事情結合在一起,來建立一個真實的電子商務應用程序的時候了。

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.
我們打算建立的這個應用程序不只是一個膚淺的演示,而是要創建一個堅固且真實的、符合當前最實用要求的應用程序。你也許會發現,由於要建立必要的基礎結構,讓事情有點慢了下來。的確,若使用Web表單,你可以更快地建立最初的功能,只要拖放一些與數據庫直接綁定的控件即可。但在MVC應用程序中所付出的這些初期投入,會給我們帶來可維護、可擴展、以及結構良好的代碼,且這些代碼對單元測試具有卓越支持。一旦我們恰當地建好了這個基本的底層結構,后面的事情就會快起來了。

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.
我們已經強調了MVC易於單元測試,也強調了單元測試是開發過程的重要部分這樣一種信念。你將通過本書看到這種信念的演繹,因為本書已經包含了與MVC關鍵特點相關的單元測試和技術的細節。

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.
我們將要使用的一些MVC特性在本書的后面有它們自己的章節。這里不會重復所有內容,我們將只告訴你對本應用程序有足夠意義的東西,並給你指明深度信息所在的章節。

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.
我們將提出建立應用程序所需要的每個步驟,以使你能夠明白,如何把MVC特性組合到一起。當我們創建視圖時,你應當特別注意。如果未采用我們所用的選項,你可能會得到奇怪的結果。為了對此進行幫助,我們每次把視圖添加到項目時,都給出了“添加視圖”對話框的圖示。

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所示。

圖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.
將解決方案命名為SportsStore,點擊“確定”按鈕創建它。一旦你創建了這個解決方案,便可以添加單個的項目。我們所需的三個項目的細節如表7-1。

Table 7-1. The Three SportsStore Projects
表7-1. 三個SportsStore項目
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
保存域實體和邏輯,通過用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應用程序的UI
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所示。

圖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所示。

圖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.
或者,如果喜歡,你可以從Ninject和Moq的web網站下載它們,然后手工地添加這些引用,如表7-2所示。我們也需要建立這些項目之間的依賴性,如表中所列的那樣。

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
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.
在“解決方案資源管理器”窗口中右擊每個項目,選擇“添加引用”,然后把(表7-2所示的)引用添加到工具庫中,或所需的其它項目中。

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.
我們打算用Ninject來創建MVC應用程序的控制器並處理DI。為此,我們需要創建一個新類,並進行配置修改。

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.
我們還沒有添加任何Ninject綁定,但已經可以使用這個AddBindings方法來做這種事情了。我們需要告訴MVC,我們希望用這個NinjectController類來創建對象,為此,需要把清單7-2所示的黑體語句添加到SportsStore.WebUI項目中的Global.asax.cs的Application_Start方法中。

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

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.
如果你在“調試”菜單中選擇“運行調試”,你將看到一個錯誤頁面。這是因為你請求了一個URL,而這個URL是與一個Ninject還沒有綁定的控制器相關的,如圖7-4所示。

圖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所示。

圖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.
我們打算從域模型開始。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.
由於這是一個電子商務應用程序,我們所需要的最明顯的域實體是產品。在SportsStore.Domain項目中創建一個新的名為Entities的文件夾,然后在其中創建一個名為Product的C#類。你可以從圖7-6看到我們所要的結構。

圖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.
你已經熟悉了Product類的內容,因為我們打算使用的是你前幾章所看到的同一個類。它包含我們所需要的明顯的屬性。編輯Product類文件,使它如清單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.
這個接口使用了IQueryable<T>接口,以便獲取Product對象的一個序列,而不需要說明數據如何存儲,或存儲在哪兒,或者如何接收它。一個使用這個IProductRepository接口的類可以獲取Product對象,而不需要知道這些對象來自哪兒,或如何傳送它們。這是存儲庫模式的本質。我們將在開發過程進行到要添加特性時,重新討論這個接口。

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.
現在,我們已經定義了一個抽象接口,我們可以繼續前進,實現持久化機制,並把它掛接到一個數據庫上。但我們打算在本章稍后再做這件事。為了能夠開始此應用程序的其它部分,我們打算先創建IProductRepository接口的一個模仿實現,並打算在NinjectControllerFactory類的AddBindings方法中做這件事,如清單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.
我們可以將本章的其余篇幅全用於建立域模型和存儲庫,而根本不接觸UI項目。但我們認為你可能覺得這很枯燥,因此,我們打算做點調整,認真地開始使用MVC框架,並在需要的時候,再把特性添加到模型和存儲庫。

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.
在本小節中,我們打算創建一個控制器和一個動作方法,它能夠顯示存儲庫中的產品細節。就目前而言,這將只針對模仿存儲庫中的數據,但稍后將對之再行探討。我們也會建立一個初始的路由配置,這樣,MVC便會知道,如何把對應用程序的請求映射到我們創建的控制器上。

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.
你可以看到,我們已經添加了一個以IProductRepository為參數的構造器。這將允許Ninject在實例化這個控制器類時,注入產品存儲庫的依賴性。下一步,我們打算添加一個動作方法,名為List,它將渲染一個顯示完整產品列表的視圖,如清單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.
通過第3章,你可能還記得,像這樣調用View方法(未指定視圖名稱)是告訴框架為這個動作方法渲染一個默認的視圖。通過把Product對象的列表傳遞給這個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.
當然,現在我們需要給List動作方法添加默認視圖。右擊List方法,並從彈出菜單中選擇“添加視圖”,將此視圖命名為List,並選中“創建強類型視圖”復選框,如圖7-7所示。

圖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.
在“模型類”下,輸入IEnumerable<SportsStore.Domain.Entities.Product>。你需要手工輸入,因為它不在下拉列表中,該下拉列表不會包含域對象的枚舉。我們稍后將使用默認的Razor布局,以使視圖有一致的外觀,因此,選中“使用布局或母版頁”復選框,但讓此文本框保持為空,如上圖所示的那樣。點擊“添加”按鈕來創建這個視圖。

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.
知道視圖中的模型含有一個IEnumerable<Product>,意味着我們可以通過一個foreach的Razor循環來創建一個列表,如清單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.
我們已經修改了頁面的標題,並創建了一個簡單的列表。注意,我們並不需要使用Razor的text或@:元素。這是因為代碼體中的每個內容行或者是一個Razor指示符,或者是以一個HTML元素開始的。

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.
現在我們要做的全部工作是告訴MVC框架,抵達網站根(http://mysite/)的請求應該被映射到ProductController類的List動作方法上。這可以通過編輯Global.asax.cs的RegisterRoutes方法做到,如清單7-9所示。

Listing 7-9. Adding the Default Route
清單7-9. 添加默認路由

public static void RegisterRoutes(RouteCollection routes) {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
        "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.
我們的所有基礎工作都已經適當就位。此刻我們有了一個含有一個動作方法的控制器,該動作方法在默認URL被請求時被調用,它依賴於存儲庫接口的一個模仿實現,該存儲庫接口生成了一些簡單的測試數據。這些測試數據被傳遞給與動作方法關聯在一起的視圖,而視圖對每個產品創建一個簡單的細節列表。如果運行這個應用程序,你可以看到如圖7-8所示的結果。

圖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.
我們已經可以顯示含有產品細節的簡單視圖,但我們顯示的只不過是模仿的IProductRepository所返回的測試數據。在可以實現一個真實的存儲庫之前,我們需要建立一個數據庫,並用一些數據填充它。

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)。

圖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所示。

圖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所示。

圖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)”復選框。

圖7-12

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)”以創建此表。

圖7-13

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將生成一個唯一的值。

圖7-14

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。

圖7-15

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. 添加一條數據庫連接

<configuration>
    <connectionStrings>
        <add name="EFDbContext" connectionString="Data Source=TITAN\SQLEXPRESS;Initial
                Catalog=SportsStore;Persist Security Info=True;User ID=adam;Password=adam"
                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.
這個連接字串連接到TITAN,這是我們的數據庫服務器(指作者的數據庫服務器 — 譯者注)。如果你已經在本地機器上安裝了SQL Server Express,那么這個連接可以如清單7-12所示。

Listing 7-12. Connecting to a Local SQL Server Express Installation
清單7-12. 連接到一個本地SQL Server Express

<configuration>
    <connectionStrings>
        <add name=”EFDbContext”
              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.
重要的是這個連接字串中name屬性的值要與這個上下文類的名字匹配,因為這樣Entity Framework才會找到我們想要與之工作的數據庫。

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.現在,我們已經做好了真正實現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.
最后一步是把Ninject對模仿存儲庫的綁定替換為對實際存儲庫的綁定。編輯SportsStore.WebUI項目中的NinjectControllerFactory類,使AddBindings方法看上去像清單7-14。

Listing 7-14. Adding the Real Repository Binding
清單7-14. 添加實際存儲庫的綁定

private void AddBindings() {
    // put additional bindings here
    // 這里放置附加綁定
    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.
新的綁定以黑體顯示。它告訴Ninject,我們希望創建EFProductRepository類的實例來對IProductRepository接口的請求進行服務。現在剩下的事情就是再次運行應用程序。結果如圖7-16所示,你可以看到,現在我們的列表包含了我們放在數據庫中的數據。

圖7-16

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.
從圖7-16可以看出,數據庫中的所有產品都顯示在一個單一的頁面上。在本小節中,我們將添加對分頁的支持,以便在一個頁面上顯示一定數目的產品,用戶可以逐頁地查看整個產品分類。要實現這一點,我們打算在Product控制器中的List方法上添加一個參數,如清單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.
添加到控制器類的內容以黑體顯示。PageSize字段指明,我們希望每頁顯示4個產品。我們稍后將用一個更好的機制來替換它。我們對List方法已經添加了一個可選參數。這意味着,如果調用不帶參數的方法(List()),該調用會被處理成就好像已經提供了我們在參數定義中指定的值(List(1))。其結果是,當我們不指定page值時,得到的是第一個頁面。LINQ讓分頁非常簡單。在List方法中,我們從存儲庫獲取Product對象,按主鍵排序,略過起始頁之前出現的產品數,然后取出由PageSize字段指定的產品個數。

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:
我們可以通過這樣的方法對分頁特性進行單元測試:創建一個模仿存儲庫,把它注入到ProductController類的構造器之中,然后調用List方法來請求一個特定的頁面。然后我們可以把得到的產品對象與我們在模仿實現中的測試數據預期的結果進行比較。詳見第6章如何建立單元測試。以下是我們為此目的創建的單元測試:

[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 // 創建一個控制器,並使頁面大小為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.
注意,獲取從控制器方法返回的數據是多么容易。我們調用結果上的Model屬性,得到在List方法中生成的IEnumerable<Product>序列。然后可以檢查該數據是否是我們想要的。在這個例子中,我們把該序列轉換成一個數據,並檢查其長度以及各個對象的值。

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:
如果你運行這個應用程序,你將看到只有四個條目顯示在頁面上。如果你想查看另一頁,你可以把查詢字串參數加到URL的末尾,像這樣:

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.
你需要修改URL的端口號,使之與你正在運行的ASP.NET開發服務器端口號匹配。運用這種查詢字串,我們可以對整個產品分類進行導航。

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.
當然,只有我們知道這么做。並未為客戶任何方式,讓他們猜出可以使用這些查詢字串參數,而且即使有,我們也可以確信,客戶不會喜歡這樣的導航方式。我們需要在每個產品列表的底部渲染一些頁面的鏈接,以使客戶可以在頁面之間導航。為了實現它,我們打算實現一個可重用的HTML輔助器方法,它類似於我們在第3章所使用的Html.TextBoxFor和Html.BeginForm方法。該輔助器方法將為我們所需要的導航鏈接生成HTML標記。

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
清單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.
視圖模型並不是域模型的一部分。它只是一種在控制器與視圖之間傳輸數據的方便的類。為了強調這一點,我們把這個類放在SportsStore.WebUI項目中,以使它與域模型的類分離開來。

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
清單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.
這個PageLinks擴展方法使用PagingInfo對象中提供的信息生成一組頁面鏈接的HTML。Func參數提供了在委托中傳遞的能力,該委托用於生成查看其它頁面的鏈接。

UNIT TEST: CREATING PAGE LINKS
單元測試:創建頁面鏈接

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:
為了測試這個PageLinks輔助器方法,我們以測試數據調用該方法,並將結果與我們所期望的HTML進行比較。該單元測試方法如下:

[TestMethod]
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.
該測試通過使用含有雙引號的文字字符串來檢查該輔助器方法的輸出。C#有很強的能力處理這種字符串,只要我們記住以@為字符串的前綴,並在用雙引號的地方用兩個雙引號("")來代替。同時也必須記住,不要把文字字符串分行,除非所比較的字符串有同樣的分行。例如,因為頁面的寬度比較窄,我們把測試方法中使用的文字分成了兩行。我們不必添加一個新行字符,要是這樣做了,測試就會失敗。

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.
記住,只當包含擴展方法的命名空間在范圍內時,其中的擴展方法才是可用的。在一個代碼文件中,這是用using語句來完成的,但對於一個Razor視圖,我們必須把一個配置條目添加到Web.config文件,或在這個視圖上添加一條@using語句。容易混淆的是,在一個Razor的MVC項目中有兩個Web.config文件:主要的一個位於應用程序的根目錄,而視圖專用的一個位於Views文件夾。我們需要進行修改的是Views/Web.config文件,如清單7-18所示。

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

<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory,
            System.Web.Mvc, Version=3.0.0.0,
            Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
        <namespaces>
            <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"/>
        </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.
在一個Razor視圖中需要引用的每一個命名空間,都需要以這種方式進行聲明,或在視圖中用@using語句進行聲明。

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.
我們更願意把控制器發送給視圖的所有數據封裝成一個單一的視圖模型類。為此,把一個新的名為ProductsListViewModel的類添加到SportsStore.WebUI的Models文件夾。這個類的內容如清單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.
現在,我們可以更新ProductController類中的List方法,以使用這個ProductsListViewModel類來給視圖提供在頁面上顯示的產品細節和分頁細節,如清單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.
這些修改把一個ProductsListViewModel對象作為模型數據傳遞給視圖。

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 // 布置 — 創建一個控制器,並使頁面大小為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:
我們也需要修改前面Can_Paginate方法中的分頁單元測試。該方法依賴於返回ViewResult的List動作方法,它的Model屬性是一個Product對象序列,但我們已經把這些數據封裝在另一個視圖模型類型中了。以下是經修改后的測試:

[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 // 創建控制器,並使頁面大小為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.
此時,視圖期望一個Product對象序列,因此,我們需要更新List.cshtml,如清單7-21所示,以處理這個新視圖模型類型。

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.
我們修改了@model指示符,以告訴Razor,現在正與一個不同的數據類型進行工作。也需要更新foreach循環,以使數據源是模型數據的Products屬性。

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.
我們已經做好了在List視圖上添加頁面鏈接的所有准備。我們創建了含有分頁信息的視圖模型,更新了控制器以使這個信息能夠傳遞給視圖,並修改了@model指示符以匹配新模型視圖類型。剩下的事就是在視圖中調用這個HTML輔助器方法了,請見清單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.
如果運行該應用程序,你將看到,我們已經添加了頁面鏈接,如圖7-17所示。樣式仍然是很基本的,本章稍后將作一些修正。此時重要的是,這個鏈接能把我們從一個頁面帶到另一個頁面,並瀏覽正在銷售的產品。

圖7-17

Figure 7-17. Displaying page navigation links
圖7-17. 顯示頁面導航鏈接

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.
如果你以前曾用過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
改進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:
我們能夠做得更好,特別是創建一種遵循可組合URL模式的方案。可組合URL是一種對用戶有意義的形式,像這樣:

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.
幸運的是,MVC很容易修改URL方案,因為它使用了ASP.NET的路由特性。我們所要做的只是把一條新路由添加到Global.asax.cs中的RegisterRoutes方法,如清單7-23所示。

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

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(帶參數的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.
重要的是把這條路由加在Default路由之前。正如你將在第11章會看到的,路由是按它們列出的順序進行處理的,而我們需要這條新路由優先於已經存在的那條。

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所示。

圖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.
我們已經建立了大量的基礎結構,而且我們的應用程序也真正地開始集合在一起了,但我們還沒有把注意力放到它的外觀上。即使這本書不是一本關於web設計或CSS的書,SportsStore應用程序設計也會因為太糟糕的格式而破壞它的技術強度。在本小節中,我們將做一些常規的事。

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.
我們打算實現一個帶有頭部的經典式兩列布局,如圖7-19所示。

圖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.
Razor布局系統等同於ASPX母板頁系統。我們可以在一個地方定義內容,然后有選擇地把它運用於個別視圖,以創建應用程序一致的外觀。我們在第5章中解釋了Razor布局是如何工作以及如何運用的。當為Product控制器創建List.cshtml視圖時,我們要求你選中了“使用一個布局”的復選框,但讓那個文本框保留為空。這便使用了默認布局,_Layout.cshtml,可以在SportsStore.WebUI項目的Views/Shared文件夾中找到它。打開這個文件並運用清單7-24所示的修改。

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

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
</head> 
<body> <div id="header"> <div class="title">SPORTS STORE(體育用品商店)</div> </div> <div id="categories"> Will put something useful here later(稍后將在此放置一些有用的東西) </div> <div id="content"> @RenderBody() </div> </body> </html>

Adding CSS Rules
添加CSS規則

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.
提示:注意,在清單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.
打開Site.css文件,並把清單7-25所示的樣式添加到文件的底部(不要刪除Site.css中已有的內容)。你不需要手工輸入,你可以下載本書伴隨的示例代碼部分的CSS附件。

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所示。

圖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.
作為本章的最后一個技巧,我們打算重構應用程序,以簡化List.cshtml視圖。我們打算創建一個分部視圖,這種分部視圖是嵌入在另一個視圖中的一個內容片段。分部視圖是自包含文件,且可以被跨視圖地重用,這有助於減少重復,尤其是需要在應用程序的幾個地方渲染同樣的數據時。

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所示。

圖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.
現在,我們需要更新Views/Products/List.cshtml,以使它使用這個分部視圖。在清單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.
我們已經去掉了之前在List.cshtml視圖中的foreach循環中的標記,並把它移到了這個新的分部視圖中。我們用Html.RenderPartial輔助器方法來調用這個分部視圖。參數是視圖的名字和視圖模型對象。

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.
切換到像這樣的分部視圖是一種很好的做法,但它並不改變應用程序的外觀。如果運行它,你將看到顯示仍和以前一樣,如圖7-22所示。

圖7-22

Figure 7-22. Applying a partial view
圖7-22. 運用分部視圖

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.
如果這章讓你感覺為一點利益做了很多事情,那么下一章將使這一問題得到一點平衡。現在,我們有了一些不顯眼的基礎元素,我們可以向前邁進,並添加各種面向客戶的特性:分類導航、購物車、以及結算過程等等。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM