本文来自:
http://www.bbsmvc.com/MVC3Framework/thread-206-1-1.html
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: 我们可以通过这样的方法来进行分页特性的单元测试:生成一个模仿存储库,把它注入到ProductController类之中,然后调用List方法来请求一个特定的页面。然后我们可以把我们得到的产品对象与我们在模仿实现中的测试数据预期的结果进行比较。详见第6章如何建立单元测试。以下是我们为此目的生成的单元测试:
Notice how easy it is to get the data that is returned from a controller method. We call the Model property on the result to get the IEnumerable<Product> sequence that we generated in the List method. We can then check that the data is what we want. In this case, we converted the sequence to an array, and checked the length and the values of the individual objects. |
Displaying Page Links
显示页面链接
If you run the application, you’ll see that there are only four items shown on the page. If you want to view another page, you can append query string parameters to the end of the URL, like this:
如果你运行这个应用程序,你将看到只有四个条目显示在页面上。如果你想查看另一页,你可以把查询字串参数加到URL的末尾,像这样:
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
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
using System;
using System.Text;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.HtmlHelpers {
public static class PagingHelpers {
public static MvcHtmlString PageLinks(this HtmlHelper html,
PagingInfo pagingInfo,
Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder();
for (int i = 1; i <= pagingInfo.TotalPages; i++) {
TagBuilder tag = new TagBuilder("a"); // Construct an <a> tag
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage)
tag.AddCssClass("selected");
result.Append(tag.ToString());
}
return MvcHtmlString.Create(result.ToString());
}
}
}
The PageLinks extension method generates the HTML for a set of page links using the information provided in a PagingInfo object. The Func parameters provides the ability to pass in a delegate that will be used to generate the links to view other pages.
PageLinks扩展方法使用PagingInfo对象中提供的信息生成一组页面链接的HTML。Func参数提供了在委派中传递的能力,该委派用于生成查看其它页面的链接。
UNIT TEST: CREATING PAGE LINKS 单元测试:生成页面链接 |
To test the PageLinks helper method, we call the method with test data and compare the results to our expected HTML. The unit test method is as follows: 为了测试PageLinks辅助方法,我们调用带有测试数据的方法并与我们所期望的HTML进行比较。单元测试如下:
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
<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
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
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: 我们需要确保控制把正确的分页数据发给视图。以下是我们针对这个而加到我们测试项目的单元测试:
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:
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) {
<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
@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. 显示分页导航连接
WHY NOT JUST USE A GRIDVIEW? 为什么不使用一个网格视图呢? |
If you’ve worked with ASP.NET before, you might think that was a lot of work for a pretty unimpressive result. It has taken us pages and pages just to get a page list. If we were using Web Forms, we could have done the same thing using the ASP.NET Web Forms GridView control, right out of the box, by hooking it up directly to our Products database table. 如果你以前用ASP.NET工作过,你也许会认为,为了一个不太令人信服的结果做了太多的工作。花了这么多篇幅只是得到了一个页面的列表。如果我们用Web表单,我们可以用ASP.NET Web表单的GridView控件,直接把它挂接到我们的Products数据库表,就可以做同样的事情了。 What we have accomplished so far doesn’t look like much, but it is very different from dragging a GridView onto a design surface. First, we are building an application with a sound and maintainable architecture that involves proper separation of concerns. Unlike the simplest use of GridView, we have not directly coupled the UI and the database together—an approach that gives quick results but that causes pain and misery over time. Second, we have been creating unit tests as we go, and these allow us to validate the behavior of our application in a natural way that’s nearly impossible with a Web Forms GridView control. Finally, bear in mind that a lot of this chapter has been given over to creating the underlying infrastructure on which the application is built. We need to define and implement the repository only once, for example, and now that we have, we’ll be able to build and test new features quickly and easily, as the following chapters will demonstrate. |
Improving the URLs
改进URL
We have the page links working, but they still use the query string to pass page information to the server, like this:
我们让页面链接起了作用,但它们仍然使用查询字串来传递页面信息给服务器,像这样:
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是一种对用户有意义的形式,比如像这样:
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方案