教程:添加排序、筛选和分页 - ASP.NET MVC 和 EF Core

在上一个教程中,已为 Student 实体实现了一组网页用于执行基本的 CRUD 操作。 在本教程中,将排序、筛选和分页功能添加到“学生索引”页。 还可以创建一个执行简单分组的页面。

在本练习中,将排序、筛选和分页功能添加到“学生索引”页。 还可以创建一个执行简单分组的页面。 下图显示了完成时页面的外观。 列标题是用户可以选择的链接,用于按该列排序。 反复选择列标题会导致排序顺序在升序和降序之间切换。

学生索引页的屏幕截图,其中包括排序、筛选和分页功能。

在本教程中,你将了解:

  • 添加链接以支持列排序
  • 添加搜索框以支持搜索
  • 向学生索引添加分页
  • 添加链接以支持分页操作
  • 为网站创建“关于”页面

先决条件

若要向 学生索引 页添加排序,请更改 Index 学生控制器的方法,并将代码添加到学生索引视图。

向 Index 方法添加排序功能

StudentsController.cs 文件中,将 Index 该方法替换为以下代码:

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

此代码接收来自 URL 中的查询字符串的 sortOrder 参数。 查询字符串值由 ASP.NET Core MVC 提供,作为操作方法的参数。 参数是字符串值:“Name”或“Date”。值(可选)后跟下划线和字符串“desc”以指定降序。 默认排序顺序为升序。

首次请求 索引 页时,没有查询字符串。 学生按照姓氏升序显示,这是 switch 语句中的 fall-through 事例所建立的默认值。 当用户选择列标题超链接时,查询字符串中会提供相应的 sortOrder 值。

视图使用这两 ViewData 个元素(NameSortParmDateSortParm)来配置具有相应查询字符串值的列标题超链接。

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

这些代码更改包含针对各元素的三元语句。 第一个语句指定如果 sortOrder 参数为 null 或为空,则元素 NameSortParm 设置为“name_desc”。否则,该元素将设置为空字符串。 通过这两个语句,视图可如下设置列标题超链接:

当前排序顺序 姓氏超链接 日期超链接
姓氏升序 降序 升序
姓氏降序 升序 升序
日期升序 升序 降序
日期降序 升序 升序

该方法使用 LINQ to Entities 指定要作为排序依据的列。 创建一个IQueryable变量在switch语句之前,在switch语句中对其进行修改,并在switch语句后调用ToListAsync方法。 当创建和修改 IQueryable 变量时,不会向数据库发送任何查询。 只有通过调用 IQueryable 之类的方法将 ToListAsync 对象转换为集合,查询才会执行。 因此,此代码生成的单个查询不会在执行return View语句之前运行。

处理大量列时,此代码可能会变得冗长。 本系列的最后一篇教程介绍了使用动态 LINQ 简化代码的高级方案>,演示如何编写代码,以便传递字符串变量中列的名称OrderBy

Views/Students/Index.cshtml 文件中的代码替换为添加列标题超链接的以下代码。 已更改的行已突出显示。

@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

此代码使用属性中 ViewData 的信息来设置具有相应查询字符串值的超链接。

运行应用,选择“ 学生 ”选项卡,然后选择“ 姓氏 ”和“ 注册日期 ”列标题以验证排序是否正常工作。

“学生索引”页的屏幕截图,其中显示按升序排序的学生姓氏。

若要向 “学生索引 ”页添加筛选,请向视图添加文本框和 “提交 ”按钮,并在方法中进行 Index 相应的更改。 在文本框中,可以输入一个字符串,以便在学生名字和姓氏字段中搜索。

向 Index 方法添加筛选支持

StudentsController.cs 文件中,将 Index 方法替换为以下代码(突出显示更改)。

public async Task<IActionResult> Index(string sortOrder, string searchString)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

您向Index方法添加了searchString参数。 从要添加到索引视图的文本框中接收搜索字符串值。 还向where语句添加了一个LINQ子句,该语句仅选择其名字或姓氏与搜索字符串匹配的学生。 仅当存在可搜索值时,才会执行添加 where 子句的语句。

确定如何使用 Where 方法

在此方案中,你将对对象Where调用IQueryable该方法,并在服务器上处理筛选器。 在某些情况下,可以将该方法 Where 作为内存中集合上的扩展方法调用。 假设你更改了对_context.Students的引用。 它引用返回集合的DbSet存储库方法,而不是调用 Entity Framework IEnumerable 方法。 结果通常相同,但在某些情况下可能有所不同。

例如,.NET Framework 对 Contains 方法的实现默认执行区分大小写的比较。 在 SQL Server 中,确定由 SQL Server 实例的排序规则设置进行。 此设置默认设置为不区分大小写。 可以使用 ToUpper 方法显式地使测试不区分大小写:Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())。 如果稍后更改代码以使用返回 IEnumerable 集合而不是 IQueryable 对象的存储库,此选项可确保结果保持不变。 (在集合上Contains调用IEnumerable该方法时,将获得 .NET Framework 实现。在对象上IQueryable调用它时,将获取数据库提供程序实现。

但是,此解决方案存在性能损耗。 该 ToUpper 代码将一个函数放入 TSQL SELECT 语句的 WHERE 子句中。 此行为可防止优化器使用索引。 鉴于 SQL 通常安装时为不区分大小写,因此最好避免使用 ToUpper 代码,直到迁移到支持不区分大小写的数据存储。

将搜索框添加到学生索引视图

Views/Student/Index.cshtml 文件中,在打开的表标记 <table> 之前立即添加突出显示的代码,以创建标题、文本框和 搜索 按钮。

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            <label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">

此代码使用 <form>标记帮助程序 添加搜索文本框和按钮。 默认情况下, <form> 标记帮助程序使用 a POST提交表单数据,这意味着参数在 HTTP 消息正文中传递,而不是作为查询字符串在 URL 中传递。 指定 HTTP GET时,表单数据作为查询字符串传入 URL,使用户可以为 URL 添加书签。 W3C 指南建议在操作不会导致更新时使用 GET

运行应用,选择“ 学生 ”选项卡,输入搜索字符串,然后选择“ 搜索 ”以验证筛选是否正常工作。

包含筛选的“学生索引”页的屏幕截图。

请注意,该 URL 包含搜索字符串。

http://localhost:5813/Students?SearchString=an

如果为此页面添加书签,则使用书签时会获得筛选列表。 将 method="get" 子句添加到 form 标记会导致创建查询字符串。

在开发阶段,如果选择列标题排序链接,则会丢失在 搜索 框中输入的筛选器值。 你将了解如何在下一部分中解决这种情况。

向学生索引添加分页

若要向 “学生索引 ”页添加分页,请创建一个 PaginatedList 类,该类使用 SkipTake 语句筛选服务器上的数据,而不是始终检索所有表行。 然后,您在 `Index` 方法中进行更多更改,并向 `Index` 视图添加分页按钮。 下图显示了分页按钮。

“学生索引”页的屏幕截图,其中包含分页链接。

在项目文件夹中,创建 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此代码中的方法采用页大小和页码,并将相应的SkipTake语句应用到IQueryable对象上。 在 IQueryable 对象上调用 ToListAsync 方法时,该方法会返回一个仅包含所请求页面的 List。 属性可用于启用或禁用HasPreviousPage”和HasNextPage”按钮。

由于构造函数不能运行异步代码,因此使用 CreateAsync 方法来创建 PaginatedList<T> 对象,而非构造函数。

向 Index 方法添加分页

StudentsController.cs 文件中,将 Index 该方法替换为以下代码。

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        pageNumber = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}

该代码向方法签名中添加一个页码参数、一个当前排序顺序参数和一个当前筛选器参数。

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? pageNumber)

首次显示页面或尚未选择分页或排序链接时,所有参数均为 null。 如果选择了分页链接,页面变量将包含要显示的页码。

命名ViewDataCurrentSort元素为视图提供当前排序顺序。 此值必须包含在分页链接中,以便排序顺序在分页时保持不变。

命名ViewDataCurrentFilter元素为视图提供当前筛选器字符串。 此值必须包含在分页链接中,才能在分页期间维护筛选器设置。 当页面显示时,该值必须还原到文本框。

如果在分页期间更改了搜索字符串,则必须将页面重置为 1,因为新筛选器可能会导致显示不同的数据。 在文本框中输入值并选择 “提交”时,将更改搜索字符串。 在此方案中, searchString 参数不为 null。

if (searchString != null)
{
    pageNumber = 1;
}
else
{
    searchString = currentFilter;
}

Index 方法最后,PaginatedList.CreateAsync 方法会将学生查询转换为支持分页的集合类型中的学生的单个页面。 然后将学生的单个页面传递给视图。

return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));

PaginatedList.CreateAsync 方法需要一个页码。 这两个问号 ?? 表示空合并运算符。 NULL 合并运算符定义可空类型的默认值。 表达式 (pageNumber ?? 1) 表示返回 pageNumber 值(如果值为 null)或返回 1(如果 pageNumber 值为 null)。

Views/Students/Index.cshtml 文件中,将现有代码替换为以下代码。 更改已被突出显示。

@model PaginatedList<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            <label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
                First Name
            </th>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</a>

页面顶部的 @model 语句指定视图现在获取的是 PaginatedList<T> 对象,而不是 List<T> 对象。

列标题链接使用查询字符串将当前搜索字符串传递给控制器,以便用户可以在筛选结果中排序:

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>

标签帮助程序显示分页按钮:

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
   Previous
</a>

运行应用并转到 “学生 ”页。

“已刷新的学生索引”页的屏幕截图,其中包含分页链接。

选择不同排序顺序的分页链接,并确保分页功能正常。 输入搜索字符串并再次尝试翻页,以验证翻页在排序和筛选的情况下是否也能正常工作。

创建“关于”页

对于 Contoso 大学网站的“关于”页,将显示每个注册日期注册了多少名学生。 此功能需要对组进行分组和简单的计算。 若要支持此行为,请完成以下任务:

  • 为需要传递给视图的数据创建一个视图模型类。
  • About在控制器中创建Home方法。
  • 创建“关于”视图。

创建视图模型

在 Models 文件夹中创建一个 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; }
    }
}

修改 Home 控制器

HomeController.cs 文件中,在文件顶部添加以下 using 语句:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;

在类的左大括号 { 后立即为数据库上下文添加类变量,并从 ASP.NET Core DI 获取上下文的实例:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }

使用以下代码添加 About 方法:

public async Task<ActionResult> About()
{
    IQueryable<EnrollmentDateGroup> data = 
        from student in _context.Students
        group student by student.EnrollmentDate into dateGroup
        select new EnrollmentDateGroup()
        {
            EnrollmentDate = dateGroup.Key,
            StudentCount = dateGroup.Count()
        };
    return View(await data.AsNoTracking().ToListAsync());
}

LINQ 语句按注册日期对学生实体进行分组。 它计算每个组中的实体数,并将结果存储在视图模型对象的集合 EnrollmentDateGroup 中。

创建“关于”视图

使用以下代码添加 Views//HomeAbout.cshtml 文件:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
    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)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

运行应用并转到“ 关于 ”页面。 表格中显示每个入学日期的学生人数。

获取代码

下载或查看已完成的应用程序

后续步骤