排序、筛选器、页面和组

ASP.NET Core 中的 Razor 页面和 EF Core - 排序、筛选、分页 - 第 3 个教程(共 8 个)

作者:Tom DykstraRick AndersonJon P Smith

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

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

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

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

“学生索引”页

添加排序

使用以下代码替换 Pages/Students/Index.cshtml.cs 中的代码,以添加排序 。

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    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)
        {
            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();
        }
    }
}

前面的代码:

  • 添加包含排序参数的属性。
  • Student 属性的名称更改为 Students
  • 替换 OnGetAsync 方法中的代码。

OnGetAsync 方法接收来自 URL 中的查询字符串的 sortOrder 参数。 该 URL(包括查询字符串)由定位点标记帮助器生成。

sortOrder 参数为“名称”或“日期”。 sortOrder 参数后面可跟“_desc”以指定降序(可选)。 默认排序顺序为升序。

如果通过“学生”链接对“索引”页发起请求,则不会有任何查询字符串 。 学生按姓氏升序显示。 按姓氏升序是 switch 语句中的默认顺序 (fall-through case)。 用户单击列标题链接时,查询字符串值中会提供相应的 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 中的代码,以添加筛选 :

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    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 Core DbSet 更改为可返回 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 中的代码,以创建“搜索”按钮和各种 chrome 。

@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:<port>/Students?SearchString=an

如果页面具有书签,该书签将包含该页面的 URL 和 SearchString 查询字符串。 form 标记中的 method="get" 会导致生成查询字符串。

目前,选中列标题排序链接时,“搜索”框中的筛选值会丢失 。 丢失的筛选值在下一部分进行修复。

添加分页

本部分将创建一个 PaginatedList 类来支持分页。 PaginatedList 类使用 SkipTake 语句在服务器上筛选数据,而不是检索所有表格行。 下图显示了分页按钮。

带有分页链接的“学生索引”页

创建 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
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        public bool HasNextPage
        {
            get
            {
                return (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> 对象;构造函数不能运行异步代码。

向 PageModel 类添加分页

替换 Students/Index.cshtml.cs 中的代码以添加分页 。

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    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 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;
            }

            int pageSize = 3;
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}

前面的代码:

  • Students 属性的类型从 IList<Student> 更改为 PaginatedList<Student>
  • OnGetAsync 方法签名添加页面索引、当前的 sortOrdercurrentFilter
  • 在 CurrentSort 属性中保存排序顺序。
  • 如果有新的搜索字符串,则将页面索引重置为 1。
  • 使用 PaginatedList 类获取 Student 实体。

出现以下情况时,OnGetAsync 接收到的所有参数均为 NULL:

  • 从“学生”链接调用页面 。
  • 用户尚未单击分页或排序链接。

单击分页链接后,页面索引变量将包含要显示的页码。

CurrentSort 属性为 Razor 页面提供当前排序顺序。 必须在分页链接中包含当前排序顺序才能在分页时保留排序顺序。

CurrentFilter 属性为 Razor 页面提供当前筛选字符串。 CurrentFilter 值:

  • 必须包含在分页链接中才能在分页过程中保留筛选设置。
  • 必须在重新显示页面时还原到文本框。

如果在分页时更改搜索字符串,页码会重置为 1。 页面必须重置为 1,因为新的筛选器会导致显示不同的数据。 输入搜索值并选择“提交”时 :

  • 搜索字符串将会更改。
  • searchString 参数不为 NULL。

PaginatedList.CreateAsync 方法会将学生查询转换为支持分页的集合类型中的单个学生页面。 单个学生页面会传递到 Razor 页面。

PaginatedList.CreateAsync 调用中的 pageIndex 之后的两个问号表示 NULL 合并运算符 NULL 合并运算符定义可为 NULL 的类型的默认值。 (pageIndex ?? 1) 表达式表示返回 pageIndex 的值(若带有值)。 如果 pageIndex 没有值,则返回 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>

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

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

带有分页链接的“学生索引”页

添加分组

本节创建“关于”页面,页面中显示每个注册日期的注册学生数。 更新需使用分组并包括以下步骤:

  • 为“关于”页使用的数据创建视图模型 。
  • 更新“关于”页以使用视图模型。

创建视图模型

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

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

“关于”页面

后续步骤

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