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. But we know this isn't a universal belief. If you don't want to unit test, that's fine with us. So, to that end, when we have something to say that is purely about unit testing or TDD, we will put it in a sidebar like this one. If you are not interested in unit testing, you can skip right over these sections, and the SportsStore application will work just fine. You don't need to do any kind of unit testing to get the benefits of ASP.NET MVC. |
Some of the MVC features we are going to use have their own chapters later in the book. Rather than duplicate everything here, we'll tell you just enough to make sense for this application and point you to the other chapter for in-depth information.
一些我们打算使用的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的源代码/下载区)。我们这里已经包括了此应用程序项目的快照,因此你可以看到应用程序如何一步步变成它所建成的样子。
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空解决方案"模板生成一个空的解决方案,该模板位于"新项目"对话框"其它项目类型"的"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.
将解决方案命名为SprotsStore,点击OK按钮生成它。一旦你生成了这个解决方案,你便可以添加个别项目。我们所需的三个项目的细节如表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将生成两个我们用不到的你可以删除的文件:SportsSDtore.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项目,并从弹出菜单中选择"设为启动项目"(你将看到其名字为粗体)。意即,当你从"调试"菜单中选择"启动调试"或"无调试运行"时,它是启动项目。
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包管理器("视图"→"其它窗口"→"包管理控制台"),并输入以下命令。记住,你可以按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
(上面只是命令示范,真正要添加的引用应当如表7-2所示 — 译者注)
Or, if you prefer, you can download Ninject and Moq from their project web sites, and then manually add the references shown in Table 7-2. We also need to set up dependencies between our projects, as listed in the table.
或者,如果你喜欢,你可以从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所示的)引用添加到工具库中,或所需的其它项目中。
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项目中生成一个名为Infastructurer的文件夹,然后生成一个名为NinjectControllerFactory的类,并编辑这个类文件与清单7-1相符。它非常类似于第6章"将Ninject运用于ASP.NET MVC"小节中所演示的类。
n Caution Throughout this chapter (and indeed the rest of the book), we usually won't give you explicit instructions when you need to add a using statement to bring a namespace into scope. To do so would be repetitious and take a lot of space, and it's pretty easy to figure it out. For example, if Visual Studio underlines a class name in a code file and warns you that "The type or namespace Product could not be found," it should be obvious that you need to add a using statement to bring the SportsStore.Domain.Entities namespace into scope in your class. The best way of doing this is to position the cursor above the type that is causing the error and press Control+. (dot). Visual Studio will figure out which namespace is required and pop up a menu that will let you add the using statement automatically. We will give you explicit instructions if you need to add a reference to an assembly in order to find a type. 注意:整个这一章(以及本书的其余部分),在需要添加一条using语句以把一个命名空间带进范围时,我们不会给出明确的说明。这样做将显得重复并占用很多篇幅,而且这很容易想象。例如,如果Visual Studio在一个代码文件的类名上有一条下划线,这是警告你,"未找到Product的类型或命名空间",这显然是需要你添加一条using语句把SportsStore.Domain.Entities命名空间纳入到你的类的范围中来。做这件事最好的办法是把光标定位到引起错误的这个类型上,按Ctrl + .(点)。Visual Studio将猜出需要哪个命名空间,并自动地弹出让你添加using语句的菜单。如果需要你添加对一个程序集的引用以找到一个类型时,我们会给出明确的说明。 |
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方法中。
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.
如果你在"调试"菜单中选择"运行调试",你将看到一个错误页面。这是因为你请求了Ninject还没有绑定的控制器的一个URL,如图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开发服务器"图标,并从弹出菜单中选择"在web浏览器中打开",如图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,或选择"生成" → "生成解决方案"),然后切换到刚才的浏览器窗口,并重新载入web页面。
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
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.
我们知道,我们需要一些从数据库中获取产品实体的方法。正如我们在第4章所解释的那样,我们希望保持逻辑从域模型实体中独立出来 — 而且我们通过使用存储库的模式来实现这一点。我们此时不需要担忧我们要如何去实现保持,但我们将为它定义一个接口来开始这一过程。
Create a new top-level folder inside the SportsStore.Domain project called Abstract and a new interface called IProductsRepository, the contents of which are shown in Listing 7-4. You can add a new interface by right-clicking the Abstract folder, selecting Add ä New Item, and selecting the Interface template.
在SprotsStore.Domain项目中生成一个新的顶级文件夹,名为Abstract,和一个名为IProduct[color=red]s[/color]Repository(这里多了一个s,应当用IProductRepository,不然与清单7-4的代码不符 — 译者注)的新接口,其内容如清单7-4所示。你可以通过右击Abstract文件夹 → "添加新项" → 选"接口"模板的方法来添加一个新接口。
Listing 7-4. The IProductRepository Interface File
using System.Linq; using SportsStore.Domain.Entities; namespace SportsStore.Domain.Abstract { public interface IProductRepository { IQueryableProducts { get; } } }
This interface uses the IQueryable
这个接口使用了IQueryable
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
private void AddBindings() { // Mock implementation of the IProductRepository Interface Mockmock = new Mock (); mock.Setup(m => m.Products).Returns(new List { new Product { Name = "Football", Price = 25 }, new Product { Name = "Surf board", Price = 179 }, new Product { Name = "Running shoes", Price = 95 } }.AsQueryable()); ninjectKernel.Bind ().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,并确保模板选项设置在"Empty"控制器上。当Visual Studio打开让你编辑的这个文件时,你可以删除已经自动添加进来的默认的动作方法,因此,你的文件看上去如清单7-6。
Listing 7-6. The Empty ProductController Class
using System.Linq; using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public ProductController(IProductRepository productRepository) { repository = productRepository; } } }
You can see that we've added a constructor that takes an IProductRepository parameter. This will allow Ninject to inject the dependency for the product repository when it instantiates the controller class. Next, we are going to add an action method, called List, which will render a view showing the complete list of products, as shown in Listing 7-7.
你可以看到,我们已经添加一个以IProductRepository为参数的构造器。这将允许Ninject在实例化控制器类时注入产品存储库的依赖性。下一步,我们打算添加一个动作方法,名为List,它将渲染一个显示完整的产品列表的视图,如清单7-7。
Listing 7-7. Adding an Action Method
using System.Linq; using System.Web.Mvc; using SportsStore.Domain.Abstract; namespace SportsStore.WebUI.Controllers { public class ProductController : Controller { private IProductRepository repository; public ProductController(IProductRepository productRepository) { repository = productRepository; } public ViewResult List() { return View(repository.Products); } } }
As you may remember from Chapter 3, calling the View method like this (without specifying a view name) tells the framework to render the default view for the action method. By passing a List of Product objects to the View method, we are providing the framework with the data with which to populate the Model object in a strongly typed view.
正如你通过第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
在"模型类"下,输入IEnumerable
Knowing that the model in the view contains an IEnumerable
知道视图中的模型含有一个IEnumerable
Listing 7-8. The List.cshtml View
@model IEnumerable@{ ViewBag.Title = "Products"; } @foreach (var p in Model) { }@p.Name
@p.Description@p.Price.ToString("c")
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元素开始的。
n Tip Notice that we converted the Price property to a string using the ToString("c") method, which renders numerical values as currency, according to the culture settings that are in effect on your server. For example, if the server is set up as en-US, then (1002.3).ToString("c") will return $1,002.30, but if the server is set to fr-FR, then the same method will return 1 002,30 €. You can change the culture setting for your server by adding a section to the Web.config 提示:注意,我们把Price属性转换成了一个使用了ToString("c")方法的字符串,它会根据你服务器的语言设置把数字值渲染成货币。例如,如果服务的设置为en-US,那么,(1002.3)ToString("c")将返回$1,002.3,但如果服务设置为fr-FR,那么同一方法将返回1002, 30 €。你可以修改服务器的语言设置,只要把以下小节添加到Web.config的 |
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
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Product", action = "List", id = UrlParameter.Optional } ); }
You can see the changes in bold—change Home to Product and Index to List, as shown in the listing. We'll cover the ASP.NET routing feature in detail in Chapter 11. For now, it's enough to know that this change directs requests for the default URL to the action method we defined.
你可以看到以黑体表示的修改 — 将Home改为Product,及将Index改为List,如上面的清单所示。我们将在第11章涉及ASP.NET路由特性的细节。现在,知道这种修改是把对默认URL的请求改到了我们所定义的动作方法,这就够了。
n Tip Notice that we have set the value of the controller in Listing 7-9 to be Product and not ProductController, which is the name of the class. This is a compulsory ASP.NET MVC naming scheme, in which controller classes always end in Controller, and you omit this part of the name when referring to the class. 提示:注意,我们在清单7-9中所设置的控制器的值已经是Product而不是ProductController,这是类的名字。这是一个强制实行的ASP.NET MVC命名模式,控制器类总是以Controller结尾,而在对这个类进行引用时,要忽略命名的这一部分。 |
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内建的数据库管理工具。通过选择"视图"菜单中选择"服务器浏览器"来打开"服务器浏览器"窗口(图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.
右击"数据连接"并从弹出菜单中选择"生成新数据库"。键入你数据库服务器的名字,并把新数据库的名字设为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.
点击OK按钮以生成这个数据库。"服务器浏览器"窗口将反映出这个新的添加。
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数据。利用服务器浏览器,展开这个你刚添加的数据库,于是你可以看到"表"条目,右击它。并从弹出选择"添加新表",如图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所示的列。对每个列,要确保选择了正确的数据类型,并取消"允许为空"复选框。
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列并选择"设为主键"。这会添加你在图7-12中所看到的小黄色钥匙。再次右击ProductID列,并选择"属性"菜单条目。在属性窗口中,并把"标识列"属性的值设置为ProductID。
n Tip Setting the Identity Column property means that SQL Server will generate a unique primary key value when we add data to this table. When using a database in a web application, it can be very difficult to generate unique primary keys because requests from users arrive concurrently. Enabling this feature means we can store new table rows and rely on SQL Server to sort out unique values for us. 提示:设置标识列属性意味着,在我们向该表添加数据时,SQL Server会生成一个唯一的主键值。当在一个web应用程序中使用一个数据库时,生成唯一主键可能是很困难的,因为用户的请求是并发出现的。使这一特性生效意味着我们可以存储新的表行数据而依靠SQL Server为我们排出唯一值。 |
When you've entered all of the columns and changed the properties, press Control+S to save the new table. You will be prompted to enter a name for the table, as shown in Figure 7-13. Set the name to Products and click OK to create the table.
当你已经键入了所有列,并修改了这些属性时,按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表,选择"显示表数据"。键入如图7-14所示的数据。你可以用Tab键从一行移到另一行。
n Note You must leave the ProductID column empty. It is an identity column so SQL Server will generate a unique value when you tab to the next row. 注:你必须让ProductID列为空。它是一个标识列,因此,当你跳到一下行时,SQL Server将生成一个唯一的值。 |
Figure 7-14. Adding data to the Products table
图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框架项目上自动地安装了Entity Framework 4.1,但我们需要为类库项目手工地完成它。
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.
下一步是生成一个把我们的简单模型与数据库关联的上下文类。在Concrete文件夹中添加一个名为EFDbContext的新类,然后编辑其内容使其如清单7-10。
Listing 7-10. The EfDbContext Class
public class EFDbContext : DbContext { public DbSetProducts { 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模型类型用来表示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
...
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
...
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
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 IQueryableProducts { 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
private void AddBindings() { // put additional bindings here ninjectKernel.Bind().To (); }
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
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))。其结果是,当我们不指定页面值时,我们得到的是第一个页面。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: [TestMethod] public void Can_Paginate() { // Arrange // - create the mock repository Mock 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 |
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标记。
To support the HTML helper, we are going to pass information to the view about the number of pages available, the current page, and the total number of products in the repository. The easiest way to do this is to create a view model, which we mentioned briefly in Chapter 4. Add the class shown in Listing 7-16, called PagingInfo, to the Models folder in the SportsStore.WebUI project. 为了支持HTML辅助方法,我们打算把关于可用页面数以及存储库中产品总数等方面的信息传递给视图。做这件事最容易的办法是生成一个视图模型,这是我们在第4章概要提到的。把清单7-16所示的、名为PagingInfo的类添加到SportsStore.WebUI项目的Models文件夹。
Listing 7-16. The PagingInfo View Model Class
using System; namespace SportsStore.WebUI.Models { public class PagingInfo { public int TotalItems { get; set; } public int ItemsPerPage { get; set; } public int CurrentPage { get; set; } public int TotalPages { get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); } } } }
A view model isn't part of our domain model. It is just a convenient class for passing data between the controller and the view. To emphasize this, we have put this class in the SportsStore.WebUI project to keep it separate from the domain model classes.
视图模型并不是域模型的一部分。它只是一种在控制器与视图之间传输数据的方便的类。为了强调这一点,我们把这个类放在SportsStore.WebUI项目中以保持它与域模型类分离。
Now that we have the view model, we can implement the HTML helper method, which we are going to call PageLinks. Create a new folder in the SportsStore.WebUI project called HtmlHelpers and add a new static class called PagingHelpers. The contents of the class file are shown in Listing 7-17. 现在,我们有了视图模型,我们可以实现这个HTML辅助方法了,我们将之称为PageLinks。在SportsStore.WebUI项目中生成一个新文件夹,名为HtmlHelpers,并添加一个新的静态类,名为PagingHelpers。类文件的内容如清单7-17所示。
Listing 7-17. The PagingHelpers Class
using System; using System.Text; using System.Web.Mvc; using SportsStore.WebUI.Models; namespace SportsStore.WebUI.HtmlHelpers { public static class PagingHelpers { public static MvcHtmlString PageLinks(this HtmlHelper html, PagingInfo pagingInfo, FuncpageUrl) { StringBuilder result = new StringBuilder(); for (int i = 1; i <= pagingInfo.TotalPages; i++) { TagBuilder tag = new TagBuilder("a"); // Construct an tag 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: [TestMethod] public void Can_Generate_Page_Links() { // Arrange - define an HTML helper - we need to do this // in order to apply the extension method HtmlHelper myHelper = null; // Arrange - create PagingInfo data PagingInfo pagingInfo = new PagingInfo { CurrentPage = 2, TotalItems = 28, ItemsPerPage = 10 }; // Arrange - set up the delegate using a lambda expression Func This test verifies the helper method output by using a literal string value that contains double quotes. C# is perfectly capable of working with such strings, as long as we remember to prefix the string with @ and use two sets of double quotes ("") in place of one set of double quotes. We must also remember not to break the literal string into separate lines, unless the string we are comparing to is similarly broken. For example, the literal we use in the test method has wrapped onto two lines because the width of a printed page is narrow. We have not added a newline character; if we did, the test would fail. |
Remember that an extension method is available for use only when the namespace that contains it is in scope. In a code file, this is done with a using statement, but for a Razor view, we must add a configuration entry to the Web.config file, or add an @using statement to the view itself. There are, confusingly, two Web.config files in a Razor MVC project: the main one, which resides in the root directory of the application project, and the view-specific one, which is in the Views folder. The change we need to make is to the Views/Web.config file and is shown in Listing 7-18.
记住,扩展方法只当包含它的命名空间在范围内时才是可用的。在一个代码文件中,它是用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
...
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语句进行声明。
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
using System.Collections.Generic; using SportsStore.Domain.Entities; namespace SportsStore.WebUI.Models { public class ProductsListViewModel { public IEnumerableProducts { 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
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 We also need to modify our earlier pagination unit test, contained in the Can_Paginate method. It relies on the List action method returning a ViewResult whose Model property is a sequence of Product objects, but we have wrapped that data inside another view model type. Here is the revised test: [TestMethod] public void Can_Paginate() { // Arrange // - create the mock repository Mock 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
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) {}@p.Name
@p.Description@p.Price.ToString("c")
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属性。
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
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) {}@p.Name
@p.Description@p.Price.ToString("c")
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x}))
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. 显示分页导航连接
WHY NOT JUST USE A GRIDVIEW? 为什么不使用一个网格视图呢? |
---|
If you've worked with ASP.NET before, you might think that was a lot of work for a pretty unimpressive result. It has taken us pages and pages just to get a page list. If we were using Web Forms, we could have done the same thing using the ASP.NET Web Forms GridView control, right out of the box, by hooking it up directly to our Products database table. What we have accomplished so far doesn't look like much, but it is very different from dragging a GridView onto a design surface. First, we are building an application with a sound and maintainable architecture that involves proper separation of concerns. Unlike the simplest use of GridView, we have not directly coupled the UI and the database together—an approach that gives quick results but that causes pain and misery over time. Second, we have been creating unit tests as we go, and these allow us to validate the behavior of our application in a natural way that's nearly impossible with a Web Forms GridView control. Finally, bear in mind that a lot of this chapter has been given over to creating the underlying infrastructure on which the application is built. We need to define and implement the repository only once, for example, and now that we have, we'll be able to build and test new features quickly and easily, as the following chapters will demonstrate. |
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
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( null, // we don't need to specify a name "Page{page}", new { Controller = "Product", action = "List" } ); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Product", action = "List", id = UrlParameter.Optional } ); }
It is important that you add this route before the Default one. As you'll see in Chapter 11, routes are processed in the order they are listed, and we need our new route to take precedence over the existing one.
重要的是你要把这个路由加在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应用程序设计也会因如此糟糕的格式而破坏它的技术强度。在本小节中,我们将做一些常规的事。
n Note In this part of the chapter, we will ask you to add CSS styles without explaining their meaning. If you want to learn more about CSS, we recommend Pro CSS and HTML Design Patterns by Michael Bowers (Apress, 2007) and Beginning HTML with CSS and HTML by David Schultz and Craig Cook (Apress, 2007). 注:本章的这部分,我们将要求你添加CSS样式而不解释它们的意义。如果你想学习更多关于CSS,我们推荐Pro CSS and HTML Design Patterns(《精通HTML与CSS设计模式》,Michael Bowers著,Apress 2007年出版)…. |
We are going to implement a classic two-column layout with a header, as shown in Figure 7-19.
我们打算实现一个带有头部的古典式两列布局,如图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
@ViewBag.Title SPORTS STOREWill put something useful here later@RenderBody()
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文件中作了引用,如下:
n Tip Notice that the CSS and JavaScript files that are referenced in Listing 7-24 are done so using the @Url.Content method. Unlike the ASPX view engine, Razor doesn't automatically interpret the tilde character (~) as a reference for the root of the application, so we must do this explicitly using the helper method. 提示:注意,在清单7-24中引用的CSS和JavaScript文件已经用@Url.Content方法做了这事。与ASPX视图引擎不同,Razor并不自动地把波浪符(~)解析为应用程序的根,因此,我们必须用辅助方法明确地做好这件事。 |
Open the Site.css file and add the styles shown in Listing 7-25 to the bottom of the file (don't remove the existing content in Site.css). You don't need to type these in by hand. You can download the CSS additions and the rest of the project as part of the code samples that accompany this book.
打开Site.css文件,并把清单7-25所示的样式添加到文件的底部(不要删除Site.css中已有的内容)。你不需要手工地输入这些,你可以下载本书伴随的例子代码部分的CSS附件。
Listing 7-25. Defining CSS
BODY { font-family: Cambria, Georgia, "Times New Roman"; margin: 0; } DIV#header DIV.title, DIV.item H3, DIV.item H4, DIV.pager A { font: bold 1em "Arial Narrow", "Franklin Gothic Medium", Arial; } DIV#header { background-color: #444; border-bottom: 2px solid #111; color: White; } DIV#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类,或手工输入可用的类名。检查“生成为部分视图”选项,如图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视图,你将看到它只含有模型视图指示符,它被设置到我们的Product域模型类。运用如清单7-26所示的修改。
Listing 7-26. Adding Markup to the ProductSummary Partial View
@model SportsStore.Domain.Entities.Product@Model.Name
@Model.Description@Model.Price.ToString("c")
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
@model SportsStore.WebUI.Models.ProductsListViewModel @{ ViewBag.Title = "Products"; } @foreach (var p in Model.Products) { Html.RenderPartial("ProductSummary", p); }@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x}))
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辅助方法来调用这个部分视图。参数是视图的名字和视图模型对象。
n Tip The RenderPartial method doesn't return HTML markup like most other helper methods. Instead, it writes content directly to the response stream, which is why we must call it like a complete line of C#, using a semicolon. This is slightly more efficient than buffering the rendered HTML from the partial view, since it will be written to the response stream anyway. If you prefer a more consistent syntax, you can use the Html.Partial method, which does exactly the same as the RenderPartial method, but returns an HTML fragment and can be used as @Html.Partial("ProductSummary", p). 提示:RenderPartial方法并不像大多数其它辅助方法那样返回HTML标记。而是,它把内容直接写到响应流,这是我们必须用一个分号,像一个完整的C#程序行一样来调用它的原因。这比缓冲已渲染的部分视图的HTML更有效一些,因为它将被写到响应流。如果你喜欢一种更一致的语法,你可以用Html.partial方法,它完成与RenderPartial方法同样的事情,但返回一个HTML片段,并能够像@Html.Partial(“ProductSummary”, p)一样来使用。 |
Switching to a partial view like this is good practice, but it doesn't change the appearance of the application. If you run it, you'll see that the display remains as before, as shown in Figure 7-22.
切换到像这样的部分视图是一种很好的实践,但它并不改变应用程序的样子。如果你运行它,你将看到的显示仍和以前一样,如图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.
如果这章让你觉得为一点利益做了很多事情,那么下一章将使这一问题得到一点平衡。现在,我们有了一些不显眼的基础元素,我们可以向前迈进,并添加各种面向客户的特性:目录导航、购物车、以及结算过程等等。