第 3 部分,ASP.NET Core 中的 Razor Pages 和 EF Core - 排序、筛选、分页

作者:Tom DykstraJeremy LiknessJon P Smith

Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor Pages Web 应用。 若要了解系列教程,请参阅第一个教程

如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。

本教程将向学生页面添加排序、筛选和分页功能。

下图显示完整的页面。 列标题是可单击的链接,可用于对列进行排序。 重复单击列标题可在升降排序顺序之间切换。

Students index page

添加排序

使用以下代码替换 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 参数为 NameDatesortOrder 参数后面可跟 _desc 以指定降序(可选)。 默认排序顺序为升序。

如果通过“学生”链接对“索引”页发起请求,则不会有任何查询字符串。 学生按姓氏升序显示。 按姓氏升序是 switch 语句中的 default。 用户单击列标题链接时,查询字符串值中会提供相应的 sortOrder 值。

Razor 页面使用 NameSortDateSort 为列标题超链接配置相应的查询字符串值:

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>

前面的代码:

  • LastNameEnrollmentDate 列标题添加超链接。
  • 使用 NameSortDateSort 中的信息为超链接设置当前的排序顺序值。
  • 将页面标题从“索引”更改为“学生”。
  • 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 类使用 SkipTake 语句在服务器上筛选数据,而不是检索所有表格行。 下图显示了分页按钮。

Students index page with paging links

创建 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 方法会提取页面大小和页码,并将相应的 SkipTake 语句应用于 IQueryable。 当在 IQueryable 上调用 ToListAsync 时,它将返回仅包含所请求页的列表。 属性 HasPreviousPageHasNextPage 用于启用或禁用“上一页”和“下一页”分页按钮 。

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 方法签名添加页面索引、当前的 sortOrdercurrentFilter
  • 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>

运行应用并导航到学生页面。

  • 为确保分页生效,请单击不同排序顺序的分页链接。
  • 要验证确保分页后可正确地排序和筛选,请输入搜索字符串并尝试分页。

students index page with paging links

分组

本部分创建 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 视图模型对象的集合中。

运行应用并导航到“关于”页面。 表格中会显示每个注册日期的学生计数。

About page

后续步骤

在下一教程中,应用将利用迁移更新数据模型。

本教程将添加排序、筛选、分组和分页功能。

下图显示完整的页面。 列标题是可单击的链接,可用于对列进行排序。 重复单击列标题可在升降和降序排序顺序之间切换。

Students index page

如果遇到无法解决的问题,请下载已完成应用

向索引页添加排序

Students/Index.cshtml.csPageModel 添加字符串,使其包含排序参数:

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.csOnGetAsync

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 页面使用 NameSortDateSort 为列标题超链接配置相应的查询字符串值:

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>

前面的代码:

  • LastNameEnrollmentDate 列标题添加超链接。
  • 使用 NameSortDateSort 中的信息为超链接设置当前的排序顺序值。

若要验证排序是否生效:

  • 运行应用并选择“学生”选项卡。
  • 单击“姓氏”。
  • 单击“注册日期”。

若要更好地了解此代码:

  • Students/Index.cshtml.cs 中,在 switch (sortOrder) 上设置断点。
  • 添加对 NameSortDateSort 的监视。
  • Students/Index.cshtml 中,在 @Html.DisplayNameFor(model => model.Student[0].LastName) 上设置断点。

单步执行调试程序。

向“学生索引”页添加搜索框

若要向“学生索引”页添加筛选:

  • 需要向 Razor 页面添加一个文本框和一个提交按钮。 文本框会针对名字或姓氏提供一个搜索字符串。
  • 页面模型随即更新以使用文本框值。

向 Index 方法添加筛选功能

使用以下代码更新 Students/Index.cshtml.csOnGetAsync

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 可能会大幅降低性能:

  1. 所有行均从 DB 服务器返回。
  2. 筛选应用于应用程序中所有返回的行。

调用 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 类使用 SkipTake 语句在服务器上筛选数据,而不是检索所有表格行。 下图显示了分页按钮。

Students index page with paging links

在项目文件夹中,使用以下代码创建 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 时,它将返回仅包含所请求页的列表。 属性 HasPreviousPageHasNextPage 用于启用或禁用“上一页”和“下一页”分页按钮 。

CreateAsync 方法用于创建 PaginatedList<T>。 构造函数不能创建 PaginatedList<T> 对象;构造函数不能运行异步代码。

向 Index 方法添加分页功能

Students/Index.cshtml.cs 中,将 Student 的类型从 IList<Student> 更新为 PaginatedList<Student>

public PaginatedList<Student> Student { get; set; }

使用以下代码更新 Students/Index.cshtml.csOnGetAsync

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);
}

上述代码会向方法签名添加页面索引、当前的 sortOrdercurrentFilter

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。

更新 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 page with paging links

若要更好地了解此代码:

  • Students/Index.cshtml.cs 中,在 switch (sortOrder) 上设置断点。
  • 添加对 NameSortDateSortCurrentSortModel.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>

运行应用并导航到“关于”页面。 表格中会显示每个注册日期的学生计数。

如果遇到无法解决的问题,请下载本阶段的已完成应用

About page

其他资源

在下一教程中,应用将利用迁移更新数据模型。