如何在 ASP.NET Core 中构建高效的分页






4.93/5 (9投票s)
学习如何使用可靠的分页控件实现高效分页。
引言
本文将解释如何使用分页仅检索所需数量的记录,并显示一个分页控件,引用总记录数。
背景
很可能,您遇到过需要从包含数千条甚至更多记录的数据源中列出几行记录的问题,但随后注意到分页是提高网站性能的重要因素。从过滤数据到从数据库中选择相关记录,再到显示分页控件,有几个但重要的步骤需要考虑才能构建一个可靠的分页系统。
创建项目
我将使用 VS2019 附带的默认 ASP.NET Core 2.2 项目模板,因此只需创建基本的 ASP.NET Core 2.2 项目并继续阅读。
在深入研究分页之前,我们需要创建一个数据源。我们的数据源必须包含大量记录,这样我们才能看到分页控件的真正好处。为了将重点放在分页主题上,我将使用框架中已包含的一个项目列表(CultureInfo
)。
打开 Pages/Index.cshtml.cs 并添加如下数据源
public class IndexModel : PageModel
{
public CultureInfo[] CulturesList { get; set; }
private CultureInfo[] Cultures { get; set; }
public IndexModel()
{
//this will act as the main data source for our project
Cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
}
public void OnGet()
{
CulturesList = Cultures;
}
}
打开 Pages/Index.cshtml
并添加以下代码以显示文化列表
<table class="table table-striped">
<thead>
<tr>
<th>LCID</th>
<th>English Name</th>
<th>Native Name</th>
<th>Culture types</th>
</tr>
</thead>
<tbody>
@foreach (var c in Model.CulturesList)
{
<tr>
<td>@c.LCID</td>
<td>@c.EnglishName</td>
<td>@c.NativeName</td>
<td>@c.CultureTypes</td>
</tr>
}
</tbody>
</table>
运行应用程序以查看初始结果
在后端处理分页
如您所见,我们正在将所有记录发送到视图,这不是一种高效的编程方式,因此我们将限制选定记录的数量,以便向视图发送更少的数据。基本上,我们需要两个变量来进行分页:
- 页码:一个变量,用于指示请求的页码
- 每页记录数:一个变量,用于指示一次应选择的记录总数
稍后,我们还将添加更多变量用于过滤。
回到 Pages/Index.cshtml.cs 文件,定义变量并修改 OnGet
,这样我们的新 IndexModel
将如下所示:
public class IndexModel : PageModel
{
public IList<CultureInfo> CulturesList { get; set; }
private CultureInfo[] Cultures { get; set; }
//page number variable
[BindProperty(SupportsGet = true)]
public int P { get; set; } = 1;
//page size variable
[BindProperty(SupportsGet = true)]
public int S { get; set; } = 10;
public IndexModel()
{
//this will act as the main data source for our project
Cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
}
public void OnGet()
{
CulturesList = Cultures
//make sure to order items before paging
.OrderBy(x=>x.EnglishName)
//skip items before current page
.Skip((P-1)*S)
//take only 10 (page size) items
.Take(S)
//call ToList() at the end to execute the query and return the result set
.ToList();
}
}
运行应用程序,您将只看到前 10 条记录。
创建分页 UI 控件
Bootstrap 提供了一个非常不错的 分页 UI 控件,但它只渲染 HTML 元素,仍然需要大量工作来知道在控件内部渲染什么以及如何渲染,例如,总记录数、最大显示的页码、搜索过滤器、根据当前页码启用/禁用上一页/下一页按钮等等。
我将使用 LazZiya.TagHelpers
nuget 包中的分页标签助手,它将为我们完成所有繁重的工作。:)
PagingTagHelper
主要需要以下参数:
page-no
:必需的int
变量,表示当前页码total-records
:必需的int
,表示数据源中的总记录数query-string-value
:string
值,如果 URL 中包含搜索过滤器,则必需page-size
:可选int
(默认为 10)query-string-key-page-no
:可选string
,表示页码的查询字符串键名。默认值为"p"
,我们将不使用此选项,因为我们在后端定义了相同的键名。query-string-key-page-size
:可选string
,表示每页记录数的查询字符串键名。默认值为"s"
,因此我们将不使用此选项,因为我们在后端也定义了相同的键名。
在此处了解有关 PagingTagHelper 的更多信息。
因此,在添加分页标签助手之前,我们需要在后端添加一个用于处理总记录数的变量。
//total number of records
public int TotalRecords { get; set; } = 0;
public void OnGet()
{
TotalRecords = Cultures.Count();
CulturesList = Cultures
//make sure to order items before paging
.OrderBy(x=>x.EnglishName)
//skip items before current page
.Skip((P-1)*S)
//take only 10 (page size) items
.Take(S)
//call ToList() at the end to execute the query and return the result set
.ToList();
}
现在我们可以处理分页标签助手了。
使用程序包管理器控制台安装 LazZiya.TagHelpers
nuget 包(确保下载最新版本)
Install-Package LazZiya.TagHelpers -Version 2.2.1
或使用 nuget 包管理器 UI
将 LazZiya.TagHelpers
添加到 _ViewImports.cshtml 页面
@addTagHelper *, LazZiya.TagHelpers
在表格下方将分页标签助手代码添加到 Index.cshtml 视图
<paging page-no="Model.P"
page-size="Model.S"
total-records="Model.TotalRecords">
</paging>
稍后,在添加一些搜索过滤器后,我们将添加 query-string-value
,现在,运行应用程序并测试分页控件的基本设置。
添加搜索过滤器
记录的基本列表已完成,现在我们将添加一些搜索过滤器以获得更实用的列表。
首先,让我们为后端添加基本的文本搜索逻辑
//variable for text search
[BindProperty(SupportsGet = true)]
public string Q { get; set; } = string.Empty;
public void OnGet()
{
var query = Cultures
//search in EnglishName and NativeName
.Where(x =>
x.EnglishName.Contains(Q, StringComparison.OrdinalIgnoreCase) ||
x.NativeName.Contains(Q, StringComparison.OrdinalIgnoreCase));
//count records that returns after the search
TotalRecords = query.Count();
CulturesList = query
//make sure to order items before paging
.OrderBy(x => x.EnglishName)
//skip items before current page
.Skip((P - 1) * S)
//take only 10 (page size) items
.Take(S)
//call ToList() at the end to execute the query and return the result set
.ToList();
}
我们定义了一个名为 "Q"
的 string
变量,它将被分配给一个搜索文本框。此外,我们修改了逻辑,使 TotalRecords
值返回搜索后的记录计数。
现在我们可以将搜索表单添加到前端
<form method="get" class="form-inline">
<input asp-for="Q" class="form-control" />
<button type="submit" class="btn btn-primary">Search</button>
</form>
确保表单方法是 "get"
,因为我们正在后端定位 OnGet()
方法,这将使我们能够共享任何编号页面的 URL。
运行应用程序并测试搜索
搜索工作正常,但如果我们点击另一个页码,我们将丢失搜索关键字!为了保留所有包含在编号页面 URL 中的查询字符串参数,我们需要如下所示向标签助手添加 query-string-value
<paging page-no="Model.P"
page-size="Model.S"
total-records="Model.TotalRecords"
query-string-value="@(Request.QueryString.Value)">
</paging>
现在搜索和分页可以很好地协同工作了。
自定义分页 UI 控件
我们的分页标签助手可以通过添加更多控件(如总页数、总记录数和每页记录数控件的标签)来自定义,修改分页标签助手代码如下,以获取更多详细信息。
<paging page-no="Model.P"
page-size="Model.S"
total-records="Model.TotalRecords"
query-string-value="@(Request.QueryString.Value)"
show-prev-next="true"
show-total-pages="true"
show-total-records="true"
show-page-size-nav="true"
show-first-numbered-page="true"
show-last-numbered-page="true">
</paging>
现在我们拥有了更实用的分页控件。
提高性能
到目前为止,我们正在返回一个 CultureInfo
项目列表,但我们在表格中只显示了几个字段!因此,通过返回仅包含显示字段的对象列表,我们可以提高内存/带宽使用率。
创建一个名为 CultureItem
的新类,并将搜索逻辑修改为返回 CultureItem
s 列表而不是 CultureInfo
//object that contains only displayed fields
public class CultureItem
{
public int LCID { get; set; }
public string EnglishName { get; set; }
public string NativeName { get; set; }
public CultureTypes CultureTypes { get; set; }
}
//return list of CultureItem
public IList<CultureItem> CulturesList { get; set; }
public void OnGet()
{
var query = Cultures
//search in EnglishName and NativeName
.Where(x =>
x.EnglishName.Contains(Q, StringComparison.OrdinalIgnoreCase) ||
x.NativeName.Contains(Q, StringComparison.OrdinalIgnoreCase))
//map the selected fields to our new object
.Select(x => new CultureItem
{
LCID = x.LCID,
EnglishName = x.EnglishName,
NativeName = x.NativeName,
CultureTypes = x.CultureTypes
});
//count records that returns after the search
TotalRecords = query.Count();
CulturesList = query
//make sure to order items before paging
.OrderBy(x => x.EnglishName)
//skip items before current page
.Skip((P - 1) * S)
//take only 10 (page size) items
.Take(S)
//call ToList() at the end to execute the query and return the result set</span>
.ToList();
}
改进搜索逻辑
我们将关键字作为一个文本字符串用于搜索,我们可以通过分割搜索关键字并删除空格和重复项来改进我们的查询。
var _keyWords = Q.Split(new[] { ' ', ',', ':' },
StringSplitOptions.RemoveEmptyEntries).Distinct();
在使用 MSSqlDb 等数据库进行搜索时,如果在可为空字段中进行搜索,如果被搜索的字段为 null
,我们可能会收到异常。为了避免在 null
字段中搜索,我们可以向搜索逻辑添加一个 null
检查条件。
var query = Cultures
//search in EnglishName and NativeName
.Where(x => _keyWords.Any(kw =>
(x.EnglishName!=null && x.EnglishName.Contains
(kw, StringComparison.OrdinalIgnoreCase)) ||
(x.NativeName != null && x.NativeName.Contains
(kw, StringComparison.OrdinalIgnoreCase))))
通过在搜索数据库时使用 AsNoTracking()
可以进一步提高性能,这样框架就不会继续跟踪选定的实体,这将有助于释放一些内存。
通用搜索表达式
在我的下一篇文章中,我将解释如何使用 EF Core 中的泛型表达式构建通用的分页和搜索方法。
历史
- 2019 年 9 月 21 日:初始版本