第 3 部分,ASP.NET Core 中的 Razor Pages 和 EF Core - 排序、筛选、分页
作者:Tom Dykstra、Jeremy Likness 和 Jon P Smith
Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor Pages Web 应用。 若要了解系列教程,请参阅第一个教程。
如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。
本教程将向学生页面添加排序、筛选和分页功能。
下图显示完整的页面。 列标题是可单击的链接,可用于对列进行排序。 重复单击列标题可在升降排序顺序之间切换。
添加排序
使用以下代码替换 Pages/Students/Index.cshtml.cs
中的代码,以添加排序。
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public IList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder)
{
// using System;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}
前面的代码:
- 需要添加
using System;
。 - 添加包含排序参数的属性。
- 将
Student
属性的名称更改为Students
。 - 替换
OnGetAsync
方法中的代码。
OnGetAsync
方法接收来自 URL 中的查询字符串的 sortOrder
参数。 该 URL 及查询字符串由定位点标记帮助器生成。
sortOrder
参数为 Name
或 Date
。 sortOrder
参数后面可跟 _desc
以指定降序(可选)。 默认排序顺序为升序。
如果通过“学生”链接对“索引”页发起请求,则不会有任何查询字符串。 学生按姓氏升序显示。 按姓氏升序是 switch
语句中的 default
。 用户单击列标题链接时,查询字符串值中会提供相应的 sortOrder
值。
Razor 页面使用 NameSort
和 DateSort
为列标题超链接配置相应的查询字符串值:
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
该代码使用 C# 条件运算符 ?:。 ?:
运算符是三元运算符,它采用三个操作数。 第一行指定当 sortOrder
为 NULL 或为空时,NameSort
设置为 name_desc
。 如果 sortOrder
不为 NULL 或不为空,则 NameSort
设置为空字符串。
通过这两个语句,页面可如下设置列标题超链接:
当前排序顺序 | 姓氏超链接 | 日期超链接 |
---|---|---|
姓氏升序 | descending | ascending |
姓氏降序 | ascending | ascending |
日期升序 | ascending | descending |
日期降序 | ascending | ascending |
该方法使用 LINQ to Entities 指定要作为排序依据的列。 此代码会初始化 switch 语句前面的 IQueryable<Student>
,并在 switch 语句中对其进行修改:
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
创建或修改 IQueryable
时,不会向数据库发送任何查询。 将 IQueryable
对象转换成集合后才能执行查询。 通过调用 IQueryable
等方法可将 ToListAsync
转换成集合。 因此,IQueryable
代码会生成单个查询,此查询直到出现以下语句才执行:
Students = await studentsIQ.AsNoTracking().ToListAsync();
OnGetAsync
可能获得包含大量可排序列的详细信息。 要了解如何通过另一种方式使用代码编写此功能,请参阅本系列教程的 MVC 版本中的使用动态 LINQ 简化代码。
向“学生索引”页添加列标题超链接
将 Students/Index.cshtml
中的代码替换为以下代码。 突出显示所作更改。
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
前面的代码:
- 向
LastName
和EnrollmentDate
列标题添加超链接。 - 使用
NameSort
和DateSort
中的信息为超链接设置当前的排序顺序值。 - 将页面标题从“索引”更改为“学生”。
- 将
Model.Student
更改为Model.Students
。
若要验证排序是否生效:
- 运行应用并选择“学生”选项卡。
- 单击列标题。
添加筛选
若要向“学生索引”页添加筛选:
- 需要向 Razor 页面添加一个文本框和一个提交按钮。 文本框会针对名字或姓氏提供一个搜索字符串。
- 页面模型随即更新以使用文本框值。
更新 OnGetAsync 方法
使用以下代码替换 Students/Index.cshtml.cs
中的代码,以添加筛选:
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public IList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder, string searchString)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
Students = await studentsIQ.AsNoTracking().ToListAsync();
}
}
前面的代码:
- 将
searchString
参数添加到OnGetAsync
方法,然后保存CurrentFilter
属性中的参数值。 从下一部分中添加的文本框中所接收搜索字符串值。 - 向 LINQ 语句添加
Where
子句。Where
子句仅选择其名字或姓氏中包含搜索字符串的学生。 只有存在要搜索的值时才执行 LINQ 语句。
IQueryable vs.IEnumerable
该代码对 IQueryable
对象调用 Where 方法,筛选在服务器上处理。 在某些情况下,应用可能会对内存中的集合调用 Where
方法作为扩展方法。 例如,假设 _context.Students
从 EF CoreDbSet
更改为可返回 IEnumerable
集合的存储库方法。 结果通常是相同的,但在某些情况下可能不同。
例如,Contains
的 .NET Framework 实现会默认执行区分大小写的比较。 在 SQL Server 中,Contains
区分大小写由 SQL Server 实例的排序规则设置决定。 SQL Server 默认为不区分大小写。 SQLite 默认为区分大小写。 可调用 ToUpper
,进行不区分大小写的显式测试:
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`
即使是对 IEnumerable
调用 Where
方法或者该方法在 SQLite 上运行,上述代码也应确保筛选不区分大小写。
如果在 IEnumerable
集合上调用 Contains
,则使用 .NET Core 实现。 如果在 IQueryable
对象上调用 Contains
,则使用数据库实现。
出于性能考虑,通常首选对 IQueryable
调用 Contains
。 数据库服务器利用 IQueryable
完成筛选。 如果先创建 IEnumerable
,则必须从数据库服务器返回所有行。
调用 ToUpper
不会对性能产生负面影响。 ToUpper
代码会在 TSQL SELECT 语句的 WHERE 子句中添加一个函数。 添加的函数会防止优化器使用索引。 如果安装的 SQL 区分大小写,则最好避免在不必要时调用 ToUpper
。
有关详细信息,请参阅 How to use case-insensitive query with Sqlite provider(如何在 Sqlite 提供程序中使用不区分大小写的查询)。
更新 Razor 页面
替换 Pages/Students/Index.cshtml
中的代码,以添加“搜索”按钮。
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
上述代码使用 <form>
标记帮助程序来添加搜索文本框和按钮。 默认情况下,<form>
标记帮助器利用 POST 提交表单数据。 借助 POST,会在 HTTP 消息正文中而不是在 URL 中传递参数。 使用 HTTP GET 时,表单数据作为查询字符串在 URL 中进行传递。 通过查询字符串传递数据时,用户可对 URL 添加书签。 W3C 指南建议应在操作不引起更新的情况下使用 GET。
测试应用:
选择“学生”选项卡并输入搜索字符串。 如果使用 SQLite,则只有在实现上述可选
ToUpper
代码时,筛选器才不区分大小写。选择“搜索”。
请注意,该 URL 包含搜索字符串。 例如:
https://localhost:5001/Students?SearchString=an
如果页面具有书签,该书签将包含该页面的 URL 和 SearchString
查询字符串。 form
标记中的 method="get"
会导致生成查询字符串。
目前,选中列标题排序链接时,“搜索”框中的筛选值会丢失。 丢失的筛选值在下一部分进行修复。
添加分页
本部分将创建一个 PaginatedList
类来支持分页。 PaginatedList
类使用 Skip
和 Take
语句在服务器上筛选数据,而不是检索所有表格行。 下图显示了分页按钮。
创建 PaginatedList 类
在项目文件夹中,使用以下代码创建 PaginatedList.cs
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;
public bool HasNextPage => PageIndex < TotalPages;
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
上述代码中的 CreateAsync
方法会提取页面大小和页码,并将相应的 Skip
和 Take
语句应用于 IQueryable
。 当在 IQueryable
上调用 ToListAsync
时,它将返回仅包含所请求页的列表。 属性 HasPreviousPage
和 HasNextPage
用于启用或禁用“上一页”和“下一页”分页按钮 。
CreateAsync
方法用于创建 PaginatedList<T>
。 构造函数不能创建 PaginatedList<T>
对象;构造函数不能运行异步代码。
向配置添加页面大小
向 appsettings.json
配置文件添加 PageSize
:
{
"PageSize": 3,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
向 IndexModel 添加分页
替换 Students/Index.cshtml.cs
中的代码以添加分页。
using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ContosoUniversity.Pages.Students
{
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
private readonly IConfiguration Configuration;
public IndexModel(SchoolContext context, IConfiguration configuration)
{
_context = context;
Configuration = configuration;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
public PaginatedList<Student> Students { get; set; }
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
CurrentFilter = searchString;
IQueryable<Student> studentsIQ = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentsIQ = studentsIQ.OrderBy(s => s.LastName);
break;
}
var pageSize = Configuration.GetValue("PageSize", 4);
Students = await PaginatedList<Student>.CreateAsync(
studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
}
}
前面的代码:
- 将
Students
属性的类型从IList<Student>
更改为PaginatedList<Student>
。 - 向
OnGetAsync
方法签名添加页面索引、当前的sortOrder
和currentFilter
。 - 在
CurrentSort
属性中保存排序顺序。 - 如果有新的搜索字符串,则将页面索引重置为 1。
- 使用
PaginatedList
类获取 Student 实体。 - 从配置中将
pageSize
设置为 3,如果配置失败,则设置为 4。
出现以下情况时,OnGetAsync
接收到的所有参数均为 NULL:
- 从“学生”链接调用页面。
- 用户尚未单击分页或排序链接。
单击分页链接后,页面索引变量将包含要显示的页码。
CurrentSort
属性为 Razor 页面提供当前排序顺序。 必须在分页链接中包含当前排序顺序才能在分页时保留排序顺序。
CurrentFilter
属性为 Razor 页面提供当前筛选字符串。 CurrentFilter
值:
- 必须包含在分页链接中才能在分页过程中保留筛选设置。
- 必须在重新显示页面时还原到文本框。
如果在分页时更改搜索字符串,页码会重置为 1。 页面必须重置为 1,因为新的筛选器会导致显示不同的数据。 输入搜索值并选择“提交”时:
- 搜索字符串将会更改。
searchString
参数不为 NULL。
PaginatedList.CreateAsync
方法会将学生查询转换为支持分页的集合类型中的单个学生页面。 单个学生页面会传递到 Razor 页面。
PaginatedList.CreateAsync
调用中的 pageIndex
之后的两个问号表示 NULL 合并运算符。 NULL 合并运算符定义可为 NULL 的类型的默认值。 若 pageIndex
具有值,则表达式 pageIndex ?? 1
返回其值,若其没有值,则表达式返回 1。
添加分页链接
将 Students/Index.cshtml
中的代码替换为以下代码。 突出显示所作更改:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Students";
}
<h2>Students</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-primary" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Students[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
列标题链接使用查询字符串将当前搜索字符串传递到 OnGetAsync
方法:
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Students[0].LastName)
</a>
分页按钮由标记帮助器显示:
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-primary @nextDisabled">
Next
</a>
运行应用并导航到学生页面。
- 为确保分页生效,请单击不同排序顺序的分页链接。
- 要验证确保分页后可正确地排序和筛选,请输入搜索字符串并尝试分页。
分组
本部分创建 About
页面,页面中显示每个注册日期的注册学生数。 更新需使用分组并包括以下步骤:
- 为
About
页使用的数据创建视图模型。 - 更新
About
页以使用视图模型。
创建视图模型
创建“Models/SchoolViewModels”文件夹。
使用以下代码创建 SchoolViewModels/EnrollmentDateGroup.cs
:
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
创建 Razor 页面
使用以下代码创建一个 Pages/About.cshtml
文件:
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model.Students)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
创建页面模型
使用以下代码更新 Pages/About.cshtml.cs
文件:
using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
public AboutModel(SchoolContext context)
{
_context = context;
}
public IList<EnrollmentDateGroup> Students { get; set; }
public async Task OnGetAsync()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
Students = await data.AsNoTracking().ToListAsync();
}
}
}
LINQ 语句按注册日期对学生实体进行分组,计算每组中实体的数量,并将结果存储在 EnrollmentDateGroup
视图模型对象的集合中。
运行应用并导航到“关于”页面。 表格中会显示每个注册日期的学生计数。
后续步骤
在下一教程中,应用将利用迁移更新数据模型。
本教程将添加排序、筛选、分组和分页功能。
下图显示完整的页面。 列标题是可单击的链接,可用于对列进行排序。 重复单击列标题可在升降和降序排序顺序之间切换。
如果遇到无法解决的问题,请下载已完成应用。
向索引页添加排序
向 Students/Index.cshtml.cs
PageModel
添加字符串,使其包含排序参数:
public class IndexModel : PageModel
{
private readonly SchoolContext _context;
public IndexModel(SchoolContext context)
{
_context = context;
}
public string NameSort { get; set; }
public string DateSort { get; set; }
public string CurrentFilter { get; set; }
public string CurrentSort { get; set; }
使用以下代码更新 Students/Index.cshtml.cs
OnGetAsync
:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
上述代码接收来自 URL 中的查询字符串的 sortOrder
参数。 该 URL(包括查询字符串)由定位点标记帮助器生成
sortOrder
参数为“名称”或“日期”。sortOrder
参数后面跟“_desc”以指定降序(可选)。 默认排序顺序为升序。
如果通过“学生”链接对“索引”页发起请求,则不会有任何查询字符串。 学生按姓氏升序显示。 按姓氏升序是 switch
语句中的默认顺序 (fall-through case)。 用户单击列标题链接时,查询字符串值中会提供相应的 sortOrder
值。
Razor 页面使用 NameSort
和 DateSort
为列标题超链接配置相应的查询字符串值:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
以下代码包含 C# 条件 ?: 运算符:
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
第一行指定当 sortOrder
为 NULL 或为空时,NameSort
设置为“name_desc”。如果 sortOrder
不为 NULL 或为空,则 NameSort
设置为空字符串。
?: operator
也称为三元运算符。
通过这两个语句,页面可如下设置列标题超链接:
当前排序顺序 | 姓氏超链接 | 日期超链接 |
---|---|---|
姓氏升序 | descending | ascending |
姓氏降序 | ascending | ascending |
日期升序 | ascending | descending |
日期降序 | ascending | ascending |
该方法使用 LINQ to Entities 指定要作为排序依据的列。 此代码会初始化 switch 语句前面的 IQueryable<Student>
,并在 switch 语句中对其进行修改:
public async Task OnGetAsync(string sortOrder)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
IQueryable<Student> studentIQ = from s in _context.Student
select s;
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
创建或修改 IQueryable
时,不会向数据库发送任何查询。 将 IQueryable
对象转换成集合后才能执行查询。 通过调用 IQueryable
等方法可将 ToListAsync
转换成集合。 因此,IQueryable
代码会生成单个查询,此查询直到出现以下语句才执行:
Student = await studentIQ.AsNoTracking().ToListAsync();
OnGetAsync
可能获得包含大量可排序列的详细信息。
向“学生索引”页添加列标题超链接
将 Students/Index.cshtml
中的代码替换为以下突出显示的代码:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
前面的代码:
- 向
LastName
和EnrollmentDate
列标题添加超链接。 - 使用
NameSort
和DateSort
中的信息为超链接设置当前的排序顺序值。
若要验证排序是否生效:
- 运行应用并选择“学生”选项卡。
- 单击“姓氏”。
- 单击“注册日期”。
若要更好地了解此代码:
- 在
Students/Index.cshtml.cs
中,在switch (sortOrder)
上设置断点。 - 添加对
NameSort
和DateSort
的监视。 - 在
Students/Index.cshtml
中,在@Html.DisplayNameFor(model => model.Student[0].LastName)
上设置断点。
单步执行调试程序。
向“学生索引”页添加搜索框
若要向“学生索引”页添加筛选:
- 需要向 Razor 页面添加一个文本框和一个提交按钮。 文本框会针对名字或姓氏提供一个搜索字符串。
- 页面模型随即更新以使用文本框值。
向 Index 方法添加筛选功能
使用以下代码更新 Students/Index.cshtml.cs
OnGetAsync
:
public async Task OnGetAsync(string sortOrder, string searchString)
{
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
CurrentFilter = searchString;
IQueryable<Student> studentIQ = from s in _context.Student
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
Student = await studentIQ.AsNoTracking().ToListAsync();
}
前面的代码:
- 向
OnGetAsync
方法添加searchString
参数。 从下一部分中添加的文本框中所接收搜索字符串值。 - 已向 LINQ 语句添加
Where
子句。Where
子句仅选择其名字或姓氏中包含搜索字符串的学生。 只有存在要搜索的值时才执行 LINQ 语句。
注意:上述代码调用 IQueryable
对象上的 Where
方法,且在服务器上处理该筛选器。 在某些情况下,应用可能会对内存中的集合调用 Where
方法作为扩展方法。 例如,假设 _context.Students
从 EF CoreDbSet
更改为可返回 IEnumerable
集合的存储库方法。 结果通常是相同的,但在某些情况下可能不同。
例如,Contains
的 .NET Framework 实现会默认执行区分大小写的比较。 在 SQL Server 中,Contains
区分大小写由 SQL Server 实例的排序规则设置决定。 SQL Server 默认为不区分大小写。 可调用 ToUpper
,进行不区分大小写的显式测试:
Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
如果上述代码改为使用 IEnumerable
,则该代码会确保结果区分大小写。 如果在 IEnumerable
集合上调用 Contains
,则使用 .NET Core 实现。 如果在 IQueryable
对象上调用 Contains
,则使用数据库实现。 从存储库返回 IEnumerable
可能会大幅降低性能:
- 所有行均从 DB 服务器返回。
- 筛选应用于应用程序中所有返回的行。
调用 ToUpper
不会对性能产生负面影响。 ToUpper
代码会在 TSQL SELECT 语句的 WHERE 子句中添加一个函数。 添加的函数会防止优化器使用索引。 如果安装的 SQL 区分大小写,则最好避免在不必要时调用 ToUpper
。
向“学生索引”页添加搜索框
在 Pages/Students/Index.cshtml
中,添加以下突出显示的代码以创建“搜索”按钮和各种 chrome。
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name:
<input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
上述代码使用 <form>
标记帮助程序来添加搜索文本框和按钮。 默认情况下,<form>
标记帮助器利用 POST 提交表单数据。 借助 POST,会在 HTTP 消息正文中而不是在 URL 中传递参数。 使用 HTTP GET 时,表单数据作为查询字符串在 URL 中进行传递。 通过查询字符串传递数据时,用户可对 URL 添加书签。 W3C 指南建议应在操作不引起更新的情况下使用 GET。
测试应用:
- 选择“学生”选项卡并输入搜索字符串。
- 选择“搜索”。
请注意,该 URL 包含搜索字符串。
http://localhost:5000/Students?SearchString=an
如果页面具有书签,该书签将包含该页面的 URL 和 SearchString
查询字符串。 form
标记中的 method="get"
会导致生成查询字符串。
目前,选中列标题排序链接时,“搜索”框中的筛选值会丢失。 丢失的筛选值在下一部分进行修复。
向“学生索引”页添加分页功能
本部分将创建一个 PaginatedList
类来支持分页。 PaginatedList
类使用 Skip
和 Take
语句在服务器上筛选数据,而不是检索所有表格行。 下图显示了分页按钮。
在项目文件夹中,使用以下代码创建 PaginatedList.cs
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity
{
public class PaginatedList<T> : List<T>
{
public int PageIndex { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
{
PageIndex = pageIndex;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
this.AddRange(items);
}
public bool HasPreviousPage => PageIndex > 1;
public bool HasNextPage => PageIndex < TotalPages;
public static async Task<PaginatedList<T>> CreateAsync(
IQueryable<T> source, int pageIndex, int pageSize)
{
var count = await source.CountAsync();
var items = await source.Skip(
(pageIndex - 1) * pageSize)
.Take(pageSize).ToListAsync();
return new PaginatedList<T>(items, count, pageIndex, pageSize);
}
}
}
上述代码中的 CreateAsync
方法会提取页面大小和页码,并将相应的 Skip
和 Take
语句应用于 IQueryable
。 当在 IQueryable
上调用 ToListAsync
时,它将返回仅包含所请求页的列表。 属性 HasPreviousPage
和 HasNextPage
用于启用或禁用“上一页”和“下一页”分页按钮 。
CreateAsync
方法用于创建 PaginatedList<T>
。 构造函数不能创建 PaginatedList<T>
对象;构造函数不能运行异步代码。
向 Index 方法添加分页功能
在 Students/Index.cshtml.cs
中,将 Student
的类型从 IList<Student>
更新为 PaginatedList<Student>
:
public PaginatedList<Student> Student { get; set; }
使用以下代码更新 Students/Index.cshtml.cs
OnGetAsync
:
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
{
CurrentSort = sortOrder;
NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
CurrentFilter = searchString;
IQueryable<Student> studentIQ = from s in _context.Student
select s;
if (!String.IsNullOrEmpty(searchString))
{
studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
studentIQ = studentIQ.OrderByDescending(s => s.LastName);
break;
case "Date":
studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
break;
default:
studentIQ = studentIQ.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}
上述代码会向方法签名添加页面索引、当前的 sortOrder
和 currentFilter
。
public async Task OnGetAsync(string sortOrder,
string currentFilter, string searchString, int? pageIndex)
出现以下情况时,所有参数均为 NULL:
- 从“学生”链接调用页面。
- 用户尚未单击分页或排序链接。
单击分页链接后,页面索引变量将包含要显示的页码。
CurrentSort
为 Razor 页面提供当前排序顺序。 必须在分页链接中包含当前排序顺序才能在分页时保留排序顺序。
CurrentFilter
为 Razor 页面提供当前的筛选字符串。 CurrentFilter
值:
- 必须包含在分页链接中才能在分页过程中保留筛选设置。
- 必须在重新显示页面时还原到文本框。
如果在分页时更改搜索字符串,页码会重置为 1。 页面必须重置为 1,因为新的筛选器会导致显示不同的数据。 输入搜索值并选择“提交”时:
- 搜索字符串将会更改。
searchString
参数不为 NULL。
if (searchString != null)
{
pageIndex = 1;
}
else
{
searchString = currentFilter;
}
PaginatedList.CreateAsync
方法会将学生查询转换为支持分页的集合类型中的单个学生页面。 单个学生页面会传递到 Razor 页面。
Student = await PaginatedList<Student>.CreateAsync(
studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
PaginatedList.CreateAsync
中的两个问号表示 NULL 合并运算符。 NULL 合并运算符定义可为 NULL 的类型的默认值。 (pageIndex ?? 1)
表达式表示返回 pageIndex
的值(若带有值)。 如果 pageIndex
没有值,则返回 1。
向“学生”Razor 页面添加分页链接
更新 Students/Index.cshtml
中的标记。 突出显示所作更改:
@page
@model ContosoUniversity.Pages.Students.IndexModel
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-page="Create">Create New</a>
</p>
<form asp-page="./Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-page="./Index">Back to full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Student[0].FirstMidName)
</th>
<th>
<a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
列标题链接使用查询字符串将当前搜索字符串传递到 OnGetAsync
方法,让用户可对筛选结果进行排序:
<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
asp-route-currentFilter="@Model.CurrentFilter">
@Html.DisplayNameFor(model => model.Student[0].LastName)
</a>
分页按钮由标记帮助器显示:
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-page="./Index"
asp-route-sortOrder="@Model.CurrentSort"
asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
asp-route-currentFilter="@Model.CurrentFilter"
class="btn btn-default @nextDisabled">
Next
</a>
运行应用并导航到学生页面。
- 为确保分页生效,请单击不同排序顺序的分页链接。
- 要验证确保分页后可正确地排序和筛选,请输入搜索字符串并尝试分页。
若要更好地了解此代码:
- 在
Students/Index.cshtml.cs
中,在switch (sortOrder)
上设置断点。 - 添加对
NameSort
、DateSort
、CurrentSort
和Model.Student.PageIndex
的监视。 - 在
Students/Index.cshtml
中,在@Html.DisplayNameFor(model => model.Student[0].LastName)
上设置断点。
单步执行调试程序。
更新“关于”页以显示学生统计信息
此步骤将更新 Pages/About.cshtml
,显示每个注册日期的已注册学生的数量。 更新需使用分组并包括以下步骤:
- 为“关于”页使用的数据创建视图模型。
- 更新“关于”页以使用视图模型。
创建视图模型
在 Models 文件夹中创建一个 SchoolViewModels 文件夹 。
在 SchoolViewModels 文件夹中,使用以下代码添加 EnrollmentDateGroup.cs
:
using System;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models.SchoolViewModels
{
public class EnrollmentDateGroup
{
[DataType(DataType.Date)]
public DateTime? EnrollmentDate { get; set; }
public int StudentCount { get; set; }
}
}
更新“关于”页面模型
ASP.NET Core 2.2 中的 Web 模板不包含“关于”页面。 如果使用的是 ASP.NET Core 2.2,请创建“关于”Razor页面。
使用以下代码更新 Pages/About.cshtml.cs
文件:
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;
namespace ContosoUniversity.Pages
{
public class AboutModel : PageModel
{
private readonly SchoolContext _context;
public AboutModel(SchoolContext context)
{
_context = context;
}
public IList<EnrollmentDateGroup> Student { get; set; }
public async Task OnGetAsync()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Student
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
Student = await data.AsNoTracking().ToListAsync();
}
}
}
LINQ 语句按注册日期对学生实体进行分组,计算每组中实体的数量,并将结果存储在 EnrollmentDateGroup
视图模型对象的集合中。
修改“关于”Razor 页面
使用以下代码替换 Pages/About.cshtml
文件中的代码:
@page
@model ContosoUniversity.Pages.AboutModel
@{
ViewData["Title"] = "Student Body Statistics";
}
<h2>Student Body Statistics</h2>
<table>
<tr>
<th>
Enrollment Date
</th>
<th>
Students
</th>
</tr>
@foreach (var item in Model.Student)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
@item.StudentCount
</td>
</tr>
}
</table>
运行应用并导航到“关于”页面。 表格中会显示每个注册日期的学生计数。
如果遇到无法解决的问题,请下载本阶段的已完成应用。
其他资源
在下一教程中,应用将利用迁移更新数据模型。
反馈
https://aka.ms/ContentUserFeedback。
即将发布:在整个 2024 年,我们将逐步淘汰作为内容反馈机制的“GitHub 问题”,并将其取代为新的反馈系统。 有关详细信息,请参阅:提交和查看相关反馈