- ASP.NET Core 中的 Razor 页面和 EF Core - 排序、筛选、分页 - 第 3 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging - 3 of 8
- 添加排序Add sorting
- 添加筛选Add filtering
- 添加分页Add paging
- 添加分组Add grouping
- 后续步骤Next steps
- 向索引页添加排序Add sorting to the Index page
- 向“学生索引”页添加搜索框Add a Search Box to the Students Index page
- 向“学生索引”页添加分页功能Add paging functionality to the Students Index page
- 向 Index 方法添加分页功能Add paging functionality to the Index method
- 向“学生”Razor 页面添加分页链接Add paging links to the student Razor Page
- 更新“关于”页以显示学生统计信息Update the About page to show student statistics
- 其他资源Additional resources
ASP.NET Core 中的 Razor 页面和 EF Core - 排序、筛选、分页 - 第 3 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Sort, Filter, Paging - 3 of 8
本文内容
作者:Tom Dykstra、Rick Anderson 和 Jon P Smith
Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。若要了解系列教程,请参阅第一个教程。
如果遇到无法解决的问题,请下载已完成的应用,然后对比该代码与按教程所创建的代码。
本教程将向学生页面添加排序、筛选和分页功能。
下图显示完整的页面。列标题是可单击的链接,可用于对列进行排序。重复单击列标题可在升降排序顺序之间切换。
添加排序Add sorting
使用以下代码替换 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 页面使用 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 简化代码。
向“学生索引”页添加列标题超链接Add column heading hyperlinks to the Student Index page
使用以下代码替换 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
。
若要验证排序是否生效:
- 运行应用并选择“学生”选项卡 。
- 单击列标题。
添加筛选Add filtering
若要向“学生索引”页添加筛选:
- 需要向 Razor 页面添加一个文本框和一个提交按钮。文本框会针对名字或姓氏提供一个搜索字符串。
- 页面模型随即更新以使用文本框值。
更新 OnGetAsync 方法Update the OnGetAsync method
使用以下代码替换 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.IEnumerableIQueryable 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 页面Update the Razor page
替换 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"
会导致生成查询字符串。
目前,选中列标题排序链接时,“搜索”框中的筛选值会丢失 。丢失的筛选值在下一部分进行修复。
添加分页Add paging
本部分将创建一个 PaginatedList
类来支持分页。PaginatedList
类使用 Skip
和 Take
语句在服务器上筛选数据,而不是检索所有表格行。下图显示了分页按钮。
创建 PaginatedList 类Create the PaginatedList class
在项目文件夹中,使用以下代码创建 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
方法会提取页面大小和页码,并将相应的 Skip
和 Take
语句应用于 IQueryable
。当在 IQueryable
上调用 ToListAsync
时,它将返回仅包含所请求页的列表。属性 HasPreviousPage
和 HasNextPage
用于启用或禁用“上一页”和“下一页”分页按钮 。
CreateAsync
方法用于创建 PaginatedList<T>
。构造函数不能创建 PaginatedList<T>
对象;构造函数不能运行异步代码。
向 PageModel 类添加分页Add paging to the PageModel class
替换 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
方法签名添加页面索引、当前的sortOrder
和currentFilter
。 - 在 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。
向 Razor 页面添加分页链接Add paging links to the Razor Page
使用以下代码替换 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>
运行应用并导航到学生页面。
- 为确保分页生效,请单击不同排序顺序的分页链接。
- 要验证确保分页后可正确地排序和筛选,请输入搜索字符串并尝试分页。
添加分组Add grouping
本节创建“关于”页面,页面中显示每个注册日期的注册学生数。更新需使用分组并包括以下步骤:
- 为“关于”页使用的数据创建视图模型 。
- 更新“关于”页以使用视图模型。
创建视图模型Create the view model
创建“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 页面Create the Razor Page
使用以下代码创建 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>
创建页面模型Create the page model
使用以下代码创建 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
视图模型对象的集合中。
运行应用并导航到“关于”页面。表格中会显示每个注册日期的学生计数。
后续步骤Next steps
在下一教程中,应用将利用迁移更新数据模型。
本教程将添加排序、筛选、分组和分页功能。
下图显示完整的页面。列标题是可单击的链接,可用于对列进行排序。重复单击列标题可在升降和降序排序顺序之间切换。
如果遇到无法解决的问题,请下载已完成应用。
向索引页添加排序Add sorting to the 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 页面使用 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
可能获得包含大量可排序列的详细信息。
向“学生索引”页添加列标题超链接Add column heading hyperlinks to the Student Index page
将 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
中的信息为超链接设置当前的排序顺序值。
若要验证排序是否生效:
- 运行应用并选择“学生”选项卡 。
- 单击“姓氏” 。
- 单击“注册日期” 。
若要更好地了解此代码:
- 请在 Student/Index.cshtml.cs 中的
switch (sortOrder)
上设置断点。 - 添加对
NameSort
和DateSort
的监视。 - 请在 Student/Index.cshtml.cs 中的
@Html.DisplayNameFor(model => model.Student[0].LastName)
上设置断点。
单步执行调试程序。
向“学生索引”页添加搜索框Add a Search Box to the Students Index page
若要向“学生索引”页添加筛选:
- 需要向 Razor 页面添加一个文本框和一个提交按钮。文本框会针对名字或姓氏提供一个搜索字符串。
- 页面模型随即更新以使用文本框值。
向 Index 方法添加筛选功能Add filtering functionality to the Index method
用以下代码更新 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 Core DbSet
更改为可返回 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
。
向“学生索引”页添加搜索框Add a Search Box to the Student Index page
在 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"
会导致生成查询字符串。
目前,选中列标题排序链接时,“搜索”框中的筛选值会丢失 。丢失的筛选值在下一部分进行修复。
向“学生索引”页添加分页功能Add paging functionality to the Students Index page
本部分将创建一个 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
{
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
方法会提取页面大小和页码,并将相应的 Skip
和 Take
语句应用于 IQueryable
。当在 IQueryable
上调用 ToListAsync
时,它将返回仅包含所请求页的列表。属性 HasPreviousPage
和 HasNextPage
用于启用或禁用“上一页”和“下一页”分页按钮 。
CreateAsync
方法用于创建 PaginatedList<T>
。构造函数不能创建 PaginatedList<T>
对象;构造函数不能运行异步代码。
向 Index 方法添加分页功能Add paging functionality to the Index method
在 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);
}
上述代码会向方法签名添加页面索引、当前的 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 页面添加分页链接Add paging links to the student Razor Page
更新 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>
运行应用并导航到学生页面。
- 为确保分页生效,请单击不同排序顺序的分页链接。
- 要验证确保分页后可正确地排序和筛选,请输入搜索字符串并尝试分页。
若要更好地了解此代码:
- 请在 Student/Index.cshtml.cs 中的
switch (sortOrder)
上设置断点。 - 添加对
NameSort
、DateSort
、CurrentSort
和Model.Student.PageIndex
的监视。 - 请在 Student/Index.cshtml.cs 中的
@Html.DisplayNameFor(model => model.Student[0].LastName)
上设置断点。
单步执行调试程序。
更新“关于”页以显示学生统计信息Update the About page to show student statistics
此步骤将更新 Pages/About.cshtml,显示每个注册日期的已注册学生的数量 。更新需使用分组并包括以下步骤:
- 为“关于”页使用的数据创建视图模型 。
- 更新“关于”页以使用视图模型。
创建视图模型Create the view model
在 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; }
}
}
更新“关于”页面模型Update the About page model
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 页面Modify the About Razor Page
将 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>
运行应用并导航到“关于”页面。表格中会显示每个注册日期的学生计数。
如果遇到无法解决的问题,请下载本阶段的已完成应用。
其他资源Additional resources
在下一教程中,应用将利用迁移更新数据模型。