C H A P T E R 7
■ ■ ■
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应用程序中所付出的这些初期投入,会给我们带来可维护、可扩展、以及结构良好的代码,且这些代码对单元测试具有卓越支持。一旦我们恰当地建好了这个基本的底层结构,后面的事情就会快起来了。
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特性组合到一起。当我们创建视图时,你应当特别注意。如果未采用我们所用的选项,你可能会得到奇怪的结果。为了对此进行帮助,我们每次把视图添加到项目时,都给出了“添加视图”对话框的图示。
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.
当然,你不需要跟着。我们已经尽可能地制作了易于理解的屏幕截图和代码清单,以防你在火车上、咖啡厅或类似的其它地方阅读本书。
We are going to create a Visual Studio solution that contains three projects. One project will contain our domain model, one will be our MVC application, and the third will contain our unit tests. To get started, let’s create an empty solution using the Visual Studio Blank Solution template, which you’ll find under the Other Project Types, Visual Studio Solutions section of the New Project dialog, as shown in Figure 7-1.
我们打算创建一个含有三个项目的Visual Studio解决方案。一个项目包含域模型、一个是MVC应用程序、而第三个则包含了单元测试。为了能够开始工作,我们用Visual Studio的“空白解决方案(Blank Solution)”模板创建一个空的解决方案,该模板位于“新建项目”对话框中“其它项目类型”的“Visual Studio解决方案”小节,如图7-1所示。
Figure 7-1. Creating a blank solution
图7-1. 创建一个空白解决方案
Give your solution the name SportsStore and click the OK button to create it. Once you’ve created the solution, you can add the individual projects. The details of the three projects we need are shown in Table 7-1.
将解决方案命名为SportsStore,点击“确定”按钮创建它。一旦你创建了这个解决方案,便可以添加单个的项目。我们所需的三个项目的细节如表7-1。
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所示。
Figure 7-2. Creating the unit test project
图7-2. 创建单元测试项目
Visual Studio will create a couple of files that we won’t use and that you can delete: the Class1.cs file in the SportsStore.Domain project and the UnitTest1.cs class in the SportsStore.UnitTests project. When you are finished, your Solution Explorer window should look like the one shown in Figure 7-3.
Visual Studio会创建两个我们用不到的、你可以删除的文件:SportsStore.Domain项目中的Class1.cs文件,和SportsStore.UnitTests项目中的UnitTest1.cs类。当完成上述工作之后,你的“解决方案资源管理器”窗口应该看上去如图7-3所示。
Figure 7-3. The projects shown in the Solution Explorer window
图7-3. 在解决方案资源管理器中显示的项目
To make debugging easier, right-click the SportsStore.WebUI project and select Set as Startup Project from the pop-up menu (you’ll see the name turn bold). This means that when you select Start Debugging or Start without Debugging from the Debug menu, it is this project that will be started.
为了使调试更容易些,右击SportsStore.WebUI项目,并从弹出菜单中选择“设为启动项目”(你将看到其名字成为粗体)。意即,当你从“调试”菜单中选择“启动调试(Start Debugging)”或“开始执行(不调试)(Start without Debugging)”时,它是应用程序的启动项目。
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所示。我们也需要建立这些项目之间的依赖性,如表中所列的那样。
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所示的)引用添加到工具库中,或所需的其它项目中。
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()); }
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所示。
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,然后从“调试”菜单中选择“停止调试”。
When you run the project from the Debug menu, Visual Studio will create a new browser window to display the application. As a speedier alternative, you can keep your application open in a stand-alone browser window. To do this, assuming you have launched the debugger at least once already, right-click the ASP.NET Development Server icon in the system tray and choose Open in Web Browser from the pop-up window, as shown in Figure 7-5.
当你通过“调试”菜单运行项目时,Visual Studio会创建一个新的浏览器窗口来显示此应用程序。一种较快的办法是,可以让应用程序在一个独立的浏览器窗口中打开。假设你至少已经运行了一次调试,可以右击“系统托盘”(屏幕右下角 — 译者注)中的“ASP.NET开发服务器(ASP.NET Development Server)”图标,并从弹出菜单中选择“在web浏览器中打开(Open in Web Browser)”,如图7-5所示。
Figure 7-5. Starting the application without using the debugger
图7-5. 不使用调试器启动应用程序
This way, each time you make a change to the application, you won’t need to launch a new debugging session to see the effect. You simply compile the solution in Visual Studio by pressing F6 or choosing Build → Build Solution, and then switch to your browser window and reload the web page.
这样,每次对应用程序作了修改之后,你不需要运行新的调试来查看效果。只要编译Visual Studio中的解决方案(按F6,或选择“创建(Build)” → “创建解决方案(Build Solution)”),然后切换到刚才的浏览器窗口,并重载该web页面(在浏览器中“刷新”页面,或按F5键 — 译者注)。
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看到我们所要的结构。
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。虽然你不一定要遵守这一约定,但我们发现,这么做有助于保持模型与控制器分离。
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对象,而不需要知道这些对象来自哪儿,或如何传送它们。这是存储库模式的本质。我们将在开发过程进行到要添加特性时,重新讨论这个接口。
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扩展方法的访问。
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便会知道,如何把对应用程序的请求映射到我们创建的控制器上。
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方法,我们是在给框架提供数据,并用这些数据填充强类型视图模型对象。
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所示。
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” />。
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这一部分。
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所示的结果。
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框架的大致开发模式。我们花了较长时间让所有事建立起来,然后快速地集合了应用程序的基本功能。
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。第三是它确实很好。早期的版本有点混乱,但当前版本十分雅致且特性丰富。
The first step is to create the database, which we are going to do using the built-in database management tools included in Visual Studio. Open the Server Explorer window (Figure 7-9) by selecting the item of the same name from the View menu.
第一步是创建数据库,我们打算用Visual Studio内建的数据库管理工具。通过在“视图”菜单中选择“服务器资源管理器(Server Explorer)”来打开“服务器资源管理器”窗口(图7-9)。
Figure 7-9. The Server Explorer window
图7-9. 服务器资源管理器窗口
Right-click Data Connections and select Create New Database from the pop-up menu. Enter the name of your database server and set the name of the new database to SportStore. If you have installed SQL Server on your development machine, the server name will be .\SQLEXPRESS, as shown in Figure 7-10.
右击“数据连接(Data Connections)”并从弹出菜单中选择“创建新数据库(Create New Database)”。键入数据库服务器名,并把新数据库的名字设为SportsStore。如果在你的开发机器上已经安装了SQL Server,此服务器名将是.\SQLEXPRESS,如图7-10所示。
Figure 7-10. Creating a new database
图7-10. 创建一个新数据库
Click the OK button to create the database. The Server Explorer window will be updated to reflect the new addition.
点击“确定”按钮以创建这个数据库。“服务器资源管理器”窗口将反映出这个新的添加。
We need only one table in our database, which we will use to store our Product data. Using Server Explorer, expand the database you just added so you can see the Table item and right-click it. Select Add New Table from the menu, as shown in Figure 7-11.
在我们的数据库中只需要一个表,用来存储Product数据。利用服务器资源管理器,展开这个你刚添加的数据库,于是你可以看到“表(Tables)”条目,右击它。并从弹出菜单中选择“添加新表(Add New Table)”,如图7-11所示。
Figure 7-11. Adding a new table
图7-11. 添加新表
A template for creating the table will open. Add the columns shown in Figure 7-12. For each of the columns, be sure to select the right data type and to uncheck the Allow Nulls options.
这会打开一个创建表的模板。添加如图7-12所示的列。对每个列,要确保选择了正确的数据类型,并取消了“允许为空(Allow Nulls)”复选框。
Figure 7-12. Creating the table columns
图7-12. 创建表列
Right-click the ProductID column and select Set Primary Key. This will add the small yellow key that you can see in Figure 7-12. Right-click the ProductID column again and select the Properties menu item. In the Properties window, set the value of the Identity Column property to ProductID.
右击ProductID列,并选择“设为主键(Set Primary Key)”。这会添加如图7-12中所看到的黄色小钥匙。再次右击ProductID列,并选择“属性(Properties)”菜单条目。在属性窗口中,把“标识列”属性的值设置为ProductID。
■ Tip Setting the Identity Column property means that SQL Server will generate a unique primary key value when we add data to this table. When using a database in a web application, it can be very difficult to generate unique primary keys because requests from users arrive concurrently. Enabling this feature means we can store new table rows and rely on SQL Server to sort out unique values for us.
提示:设置标识列属性意味着,在向该表添加数据时,SQL Server会生成一个唯一的主键值。当在一个web应用程序中使用一个数据库时,生成唯一主键可能是很困难的,因为用户的请求是并发出现的。启用这一特性意味着,我们可以存储新的表行数据,并依靠SQL Server为我们排出唯一值。
When you’ve entered all of the columns and changed the properties, press Control+S to save the new table. You will be prompted to enter a name for the table, as shown in Figure 7-13. Set the name to Products and click OK to create the table.
当你已经键入了所有列并修改了这些属性时,按Ctrl + S来保存这个新表。这将提示你输入这个表的名字,如图7-13所示。将此名设为Products,点击“确定(OK)”以创建此表。
Figure 7-13. Namings the database table
图7-13. 命名数据库表
We are going to manually add some data to the database so that we have something to work with until we add the catalog administration features in Chapter 9. In the Solution Explorer window, expand the Tables item of the SportsStore database, right-click the Products table, and select Show Table Data. Enter the data shown in Figure 7-14. You can move from row to row by using the Tab key.
我们打算手工地将一些数据添加到该数据库,以使我们在第9章添加目录管理特性之前,有一些与之工作的东西。在“解决方案资源管理器”窗口中(应当是“服务器资源管理器”窗口 — 译者注),展开SportsStore数据库的“表”条目,右击Products表,选择“显示表数据(Show Table Data)”。键入如图7-14所示的数据。你可以用Tab键从一行移到另一行。
■ Note You must leave the ProductID column empty. It is an identity column so SQL Server will generate a unique value when you tab to the next row.
注:你必须让ProductID列为空。它是一个标识列,因此,当你跳到一下行时,SQL Server将生成一个唯一的值。
Figure 7-14. Adding data to the Products table
图7-14. 对Products表添加数据
Version 4.1 of the Entity Framework includes a nice feature called code-first. The idea is that we can define the classes in our model and then generate a database from those classes.
Entity Framework 4.1版包含了一个叫做code-first(代码先行)的很好的特性。其思想是我们可以先定义模型中的类,然后再通过这些类生成一个数据库。
This is great for greenfield development projects, but these are few and far between. Instead, we are going to show you a variation on code-first, where we associate our model classes with an existing database. The first step is to add Entity Framework version 4.1 to our SportsStore.Domain project. The MVC 3 Tools Update that we installed in Chapter 2 automatically installs Entity Framework 4.1 on MVC Framework projects, but we need to do it manually for class library projects.
这很适合于绿地(greenfield)开发项目,但这些项目并不多见。因此,我们打算给你演示代码先行的一种变异,以此把模型类与现有的数据库关联在一起。第一步是把Entity Framework 4.1添加到我们的SportsStore.Domain项目。在第2章安装的“MVC 3工具更新(MVC 3 Tools Update)”自动地在“MVC框架项目”上安装了Entity Framework 4.1,但我们需要为“类库项目”手工地安装它(意即,对于SportsStore.WebUI项目(MVC框架项目),EF是被自动安装的,而对于SportsStore.Domain项目(类库项目),需要手工安装EF — 译者注)。
Right-click References and select Add Library Package Reference from the pop-up menu. Search or scroll down the list until you find the EntityFramework package, as shown in Figure 7-15, and then click the Install button. Visual Studio will download and install the latest Entity Framework version.
右击“引用”并从弹出菜单中选择“添加库包引用”。搜索或滚动列表,直到你找到EntityFramework包,如图7-15所示,然后点击“Install”按钮。Visual Studio将下载并安装最新版的Entity Framework。
Figure 7-15. Adding the EntityFramework library package
图7-15. 添加Entity Framework库包
The next step is to create a context class that will associate our simple model with the database. Add a new class called EFDbContext in the Concrete folder, and then edit the contents so that they match Listing 7-10.
下一步是创建一个将我们的简单模型与数据库关联的上下文类(context class)。在Concrete文件夹中添加一个名为EFDbContext的新类,然后编辑其内容使其如清单7-10。
Listing 7-10. The EFDbContext Class
清单7-10. EFDbContext类
public class EFDbContext : DbContext { public DbSet<Product> Products { get; set; } }
To take advantage of the code-first feature, we need to create a class that is derived from System.Data.Entity.DbContext. This class then defines a property for each table that we want to work with. The name of the property specifies the table, and the type parameter of the DbSet result specifies the model that the Entity Framework should use to represent rows in that table. In our case, the property name is Products and the type parameter is Product. We want the Product model type to be used to represent rows in the Products table.
为了利用代码先行特性,我们需要创建一个派生于System.Data.Entity.DbContext的类。这个类然后为每个我们要与之工作的表定义一个属性。属性名指定为表名,并把DbSet结果的类型参数指定为实体框架应该用来表示表行的模型。在我们的例子中,该属性名是Products(数据库中的表名 — 译者注),而参数类型是Product(用来表示该表中一行数据的模型类(Product类)的类型(Product) — 译者注)。意即我们希望用Product模型类型来表示Products表的各个行。
We need to tell the Entity Framework how to connect to the database, and we do that by adding a database connection string to the Web.config file in the SportsStore.WebUI project with the same name as the context class, as shown in Listing 7-11.
我们需要告诉Entity Framework如何连接到数据库,要完成此事,只要在SportsStore.WebUI项目的Web.config文件中以这个上下文类同样的名字添加一条数据库连接字串即可,如清单7-11所示。
Listing 7-11. Adding a Database Connection
清单7-11. 添加一条数据库连接
<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才会找到我们想要与之工作的数据库。
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所示,你可以看到,现在我们的列表包含了我们放在数据库中的数据。
Figure 7-16. The result of implementing the real repository
图7-16. 实现实际存储库的结果
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字段指定的产品个数。
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>序列。然后可以检查该数据是否是我们想要的。在这个例子中,我们把该序列转换成一个数据,并检查其长度以及各个对象的值。
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参数提供了在委托中传递的能力,该委托用于生成查看其它页面的链接。
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对象作为模型数据传递给视图。
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所示。样式仍然是很基本的,本章稍后将作一些修正。此时重要的是,这个链接能把我们从一个页面带到另一个页面,并浏览正在销售的产品。
Figure 7-17. Displaying page navigation links
图7-17. 显示页面导航链接
If you’ve worked with ASP.NET before, you might think that was a lot of work for a pretty unimpressive result. It has taken us pages and pages just to get a page list. If we were using Web Forms, we could have done the same thing using the ASP.NET Web Forms GridView control, right out of the box, by hooking it up directly to our Products database table.
如果你以前曾用过ASP.NET,你也许会认为,为了一个不起眼的结果做了太多的工作。花了这么多篇幅只是得到了一个页面的列表。如果我们使用Web表单,可以用ASP.NET Web表单的GridView控件,直接把它挂接到Products数据库表,就可以做同样的事情了。
What we have accomplished so far doesn’t look like much, but it is very different from dragging a GridView onto a design surface. First, we are building an application with a sound and maintainable architecture that involves proper separation of concerns. Unlike the simplest use of GridView, we have not directly coupled the UI and the database together—an approach that gives quick results but that causes pain and misery over time. Second, we have been creating unit tests as we go, and these allow us to validate the behavior of our application in a natural way that’s nearly impossible with a Web Forms GridView control.
到目前为止我们所完成的看上去并不太多,但它与把一个GridView拖拽到一个设计界面完全不同。首先,我们是在建立一个彻底可维护的、包含了恰当关注分离体系结构的应用程序。与GridView最简单的使用不同,我们没有把UI和数据库直接耦合在一起 — 耦合可以快速得到结果,但会长期痛苦。其次,随着事情的进行,我们一直在创建单元测试,这让我们能够以自然的方式去检验应用程序的行为,这对Web表单的GridView控件几乎是不可能的。
Finally, bear in mind that a lot of this chapter has been given over to creating the underlying infrastructure on which the application is built. We need to define and implement the repository only once, for example, and now that we have, we’ll be able to build and test new features quickly and easily, as the following chapters will demonstrate.
最后,记住本章的很多内容已经放弃了创建应用程序赖以建立的基本的体系结构。例如,我们只需要对存储库进行一次定义和实现,而现在我们做到了,我们将能够快速而容易地建立和测试新特性,正如以下章节将演示的那样。
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所示。
Figure 7-18. The new URL scheme displayed in the browser
图7-18. 显示在浏览器中的新URL方案
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所示。
Figure 7-19. The design goal for the SportsStore application
图7-19. SportsStore应用程序的设计目标
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>
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所示。
Figure 7-20. The design-enhanced SportsStore application
图7-20. 增强设计的SportsStore应用程序
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所示。
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所示。
Figure 7-22. Applying a partial view
图7-22. 运用分部视图
In this chapter, we have built most of the core infrastructure for the SportsStore application. It doesn’t have many features that you could demonstrate to a client at this point, but behind the scenes, we have the beginnings of a domain model, with a product repository that is backed by SQL Server and the Entity Framework. We have a single controller, ProductController, that can produce paginated lists of products, and we have set up DI and a clean and friendly URL scheme.
本章我们已经建立了SportsStore应用程序最核心的基础结构。它此刻并没有很多可以给客户端演示的特性,但在幕后,我们有了基本的域模型,它带有一个产品存储库,其背后有SQL Server和Entity Framework的支持。我们还有了一个控制器,ProductController,它可以产生分页的产品列表,并且我们已经建立了DI和一个清晰友好的URL方案。
If this chapter felt like a lot of setup for little benefit, then the next chapter will balance the equation. Now that we have the fundamental elements out of the way, we can forge ahead and add all of the customer-facing features: navigation by category, a shopping cart, and a checkout process.
如果这章让你感觉为一点利益做了很多事情,那么下一章将使这一问题得到一点平衡。现在,我们有了一些不显眼的基础元素,我们可以向前迈进,并添加各种面向客户的特性:分类导航、购物车、以及结算过程等等。