实现高效数据分页
这是免费 “NerdDinner”应用程序教程 的第 8 步,该教程演练了如何使用 ASP.NET MVC 1 生成小型但完整的 Web 应用程序。
步骤 8 显示如何向 /Dinners URL 添加分页支持,这样我们一次只显示 10 个即将到来的晚餐,而不是一次显示 1000 次晚餐,并允许最终用户以 SEO 友好的方式在整个列表中翻页和转发。
NerdDinner 步骤 8:分页支持
如果我们的网站成功,它将有数千个即将到来的晚餐。 我们需要确保 UI 缩放来处理所有这些晚餐,并允许用户浏览它们。 为此,我们将向 /Dinners URL 添加分页支持,这样我们一次只显示 10 个即将到来的晚餐,而不是一次显示 1000 次晚餐,并允许最终用户以 SEO 友好的方式在整个列表中翻页和转发。
Index () 操作方法回顾
DinnersController 类中的 Index () 操作方法当前如下所示:
//
// GET: /Dinners/
public ActionResult Index() {
var dinners = dinnerRepository.FindUpcomingDinners().ToList();
return View(dinners);
}
向 /Dinners URL 发出请求时,它会检索所有即将举行的晚餐的列表,然后呈现所有晚餐的列表:
了解 IQueryable<T>
IQueryable<T> 是 LINQ 作为 .NET 3.5 的一部分引入的接口。 它支持功能强大的“延迟执行”方案,我们可以利用这些方案来实现分页支持。
在 DinnerRepository 中,我们将从 FindUpcomingDinners () 方法返回 IQueryable<Dinner> 序列:
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
FindUpcomingDinners () 方法返回的 IQueryable<Dinner> 对象封装查询,以便使用 LINQ to SQL 从数据库中检索 Dinner 对象。 重要的是,在我们尝试访问/循环访问查询中的数据之前,或者直到我们对其调用 ToList () 方法,它不会对数据库执行查询。 调用 FindUpcomingDinners () 方法的代码可以选择在执行查询之前将其他“链接”操作/筛选器添加到 IQueryable<Dinner> 对象。 然后,LINQ to SQL 足够智能,在请求数据时针对数据库执行组合查询。
若要实现分页逻辑,我们可以更新 DinnersController 的 Index () 操作方法,使其在调用 ToList () 之前将其他“Skip”和“Take”运算符应用于返回的 IQueryable<Dinner> 序列:
//
// GET: /Dinners/
public ActionResult Index() {
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();
return View(paginatedDinners);
}
上述代码跳过数据库中前 10 个即将到来的晚餐,然后返回 20 个晚餐。 LINQ to SQL 足够智能,可以构造一个优化的 SQL 查询,该查询在 SQL 数据库(而不是 Web 服务器)中执行此跳过逻辑。 这意味着,即使数据库中有数百万即将推出的 Dinners,也只会在此请求中检索所需的 10 个, (使其高效且可缩放) 。
向 URL 添加“页面”值
我们不希望对特定页面范围进行硬编码,而是希望 URL 包含指示用户请求的 Dinner 范围的“页面”参数。
使用 Querystring 值
下面的代码演示了如何更新 Index () 操作方法以支持查询字符串参数并启用 /Dinners?page=2 等 URL:
//
// GET: /Dinners/
// /Dinners?page=2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
上述 Index () 操作方法具有名为“page”的参数。 参数声明为可以为 null 的整数, (int 是什么?指示) 。 这意味着 /Dinners?page=2 URL 将导致值“2”作为参数值传递。 没有查询字符串值的 /Dinners URL () 将导致传递 null 值。
我们将页面值乘以页面大小 (在本例中为 10 行) 来确定要跳过的晚餐数。 我们使用的是 C# null“合并”运算符 (??) 在处理可以为 null 的类型时非常有用。 如果 page 参数为 null,则上述代码将页面的值指定为 0。
使用嵌入的 URL 值
使用查询字符串值的替代方法是在实际 URL 本身中嵌入页面参数。 例如: /Dinners/Page/2 或 /Dinners/2。 ASP.NET MVC 包含功能强大的 URL 路由引擎,可轻松支持此类方案。
我们可以注册自定义路由规则,用于将任何传入 URL 或 URL 格式映射到所需的任何控制器类或操作方法。 只需在项目中打开 Global.asax 文件即可:
然后使用 MapRoute () 帮助程序方法(例如对路由的第一次调用)注册新的映射规则。MapRoute () 如下:
public void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"UpcomingDinners", // Route name
"Dinners/Page/{page}", // URL with params
new { controller = "Dinners", action = "Index" } // Param defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with params
new { controller="Home", action="Index",id="" } // Param defaults
);
}
void Application_Start() {
RegisterRoutes(RouteTable.Routes);
}
上面我们注册了一个名为“UpcomingDinners”的新路由规则。 我们表示其 URL 格式为“Dinners/Page/{page}”,其中 {page} 是嵌入 URL 中的参数值。 MapRoute () 方法的第三个参数指示应将此格式匹配的 URL 映射到 DinnersController 类上的 Index () 操作方法。
我们可以在 Querystring 方案中使用与之前完全相同的 Index () 代码- 不同之处在于,现在“页面”参数将来自 URL,而不是 querystring:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
现在,当我们运行应用程序并键入 /Dinners 时,我们将看到前 10 个即将到来的晚餐:
当我们键入 /Dinners/Page/1 时,我们将看到下一页的晚餐:
添加页面导航 UI
完成分页方案的最后一步是在视图模板中实现“下一步”和“上一个”导航 UI,使用户能够轻松跳过 Dinner 数据。
若要正确实现此功能,我们需要知道数据库中的 Dinners 总数,以及此转换到的数据页数。 然后,我们需要计算当前请求的“page”值是位于数据的开头还是末尾,并相应地显示或隐藏“上一个”和“下一个”UI。 我们可以在 Index () 操作方法中实现此逻辑。 或者,我们可以向项目添加一个帮助程序类,该类以更易重用的方式封装此逻辑。
下面是一个简单的“PaginatedList”帮助程序类,该类派生自内置于.NET Framework的 List<T> 集合类。 它实现一个可重用的集合类,该类可用于对 IQueryable 数据的任何序列进行分页。 在我们的 NerdDinner 应用程序中,我们将针对 IQueryable<Dinner> 结果使用它,但在其他应用程序方案中,它同样容易用于 IQueryable<Product> 或 IQueryable<Customer> 结果:
public class PaginatedList<T> : List<T> {
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = source.Count();
TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);
this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}
public bool HasPreviousPage {
get {
return (PageIndex > 0);
}
}
public bool HasNextPage {
get {
return (PageIndex+1 < TotalPages);
}
}
}
请注意上面它如何计算并公开“PageIndex”、“PageSize”、“TotalCount”和“TotalPages”等属性。 然后,它还公开两个帮助程序属性“HasPreviousPage”和“HasNextPage”,这些属性指示集合中的数据页是位于原始序列的开头还是末尾。 上述代码将导致运行两个 SQL 查询 - 第一个查询检索 Dinner 对象总数的计数, (这不返回对象 - 而是执行返回整数) 的“SELECT COUNT”语句,第二个语句仅从数据库中检索所需的数据行以获取当前页数据。
然后,我们可以更新 DinnersController.Index () 帮助程序方法,以从 DinnerRepository.FindUpcomingDinners () 结果创建 PaginatedList<Dinner> ,并将其传递到视图模板:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);
return View(paginatedDinners);
}
然后,我们可以更新 \Views\Dinners\Index.aspx 视图模板以继承自 ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> 而不是 ViewPage<IEnumerable<Dinner>>,然后将以下代码添加到 view-template 底部以显示或隐藏下一个和上一个导航 UI:
<% if (Model.HasPreviousPage) { %>
<%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>
<% } %>
<% if (Model.HasNextPage) { %>
<%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>
<% } %>
请注意以上是如何使用 Html.RouteLink () 帮助程序方法来生成超链接的。 此方法类似于我们之前使用的 Html.ActionLink () 帮助程序方法。 区别在于,我们使用在 Global.asax 文件中设置的“UpcomingDinners”路由规则生成 URL。 这可确保我们将生成格式为 /Dinners/Page/{page} 的 Index () 操作方法的 URL ,其中 {page} 值是我们基于当前 PageIndex 在上面提供的变量。
现在,当我们再次运行应用程序时,我们会在浏览器中一次看到 10 顿晚餐:
我们还在页面底部有 <<< 和 >>> 导航 UI,它允许我们使用搜索引擎可访问的 URL 向前和向后跳过数据:
附带主题:了解 IQueryable<T 的含义> |
---|
IQueryable<T> 是一项非常强大的功能,支持各种有趣的延迟执行方案, (例如分页和基于组合的查询) 。 与所有强大的功能一样,你需要小心如何使用它,并确保它不会被滥用。 请务必认识到,从存储库返回 IQueryable<T> 结果可使调用代码追加到链接运算符方法上,从而参与最终的查询执行。 如果不想提供此功能的调用代码,则应返回 IList<T> 或 IEnumerable<T> 结果 - 其中包含已执行的查询的结果。 对于分页方案,这需要将实际数据分页逻辑推送到所调用的存储库方法中。 在此方案中,我们可能会更新 FindUpcomingDinners () 查找器方法,以具有返回 PaginatedList: PaginatedList< Dinner> FindUpcomingDinners (int pageIndex 的签名, int pageSize) { } 或返回 IList<Dinner>,并使用“totalCount”out 参数返回 Dinners 的总计数: IList<Dinner> FindUpcomingDinners (int pageIndex, int pageSize, out int totalCount) { } |
下一步
现在,让我们看看如何向应用程序添加身份验证和授权支持。