用ASP.NET Core MVC 和 EF Core 构建Web应用 (三)
在上一节中,已为 Student 实体实现了一组网页用于执行基本的 CRUD 操作。 在本节中,将向学生索引页添加排序、筛选和分页功能。 同时,还将创建一个执行简单分组的页面。
向学生索引页添加列排序链接
要向学生索引页添加排序功能,需更改学生控制器的 Index
方法并将代码添加到学生索引视图。
向 Index 方法添加排序功能
在 StudentsController.cs 中,将 Index
方法替换为以下代码:
1 public async Task<IActionResult> Index(string sortOrder) 2 { 3 ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; 4 ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date"; 5 var students = from s in _context.Students 6 select s; 7 switch (sortOrder) 8 { 9 case "name_desc": 10 students = students.OrderByDescending(s => s.LastName); 11 break; 12 case "Date": 13 students = students.OrderBy(s => s.EnrollmentDate); 14 break; 15 case "date_desc": 16 students = students.OrderByDescending(s => s.EnrollmentDate); 17 break; 18 default: 19 students = students.OrderBy(s => s.LastName); 20 break; 21 } 22 return View(await students.AsNoTracking().ToListAsync()); 23 }
此代码接收来自 URL 中的查询字符串的 sortOrder
参数。 查询字符串值由 ASP.NET Core MVC 提供,作为操作方法的参数。 该参数将是一个字符串,可为“Name”或“Date”,可选择后跟下划线和字符串“desc”来指定降序。 默认排序顺序为升序。
首次请求索引页时,没有任何查询字符串。 学生按照姓氏升序显示,这是 switch
语句中的 fall-through 事例所建立的默认值。 当用户单击列标题超链接时,查询字符串中会提供相应的 sortOrder
值。
视图使用两个 ViewData
元素(NameSortParm 和 DateSortParm)来为列标题超链接配置相应的查询字符串值。
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
这些是三元语句。 第一个指定如果 sortOrder
参数为 NULL 或空,则应将 NameSortParm 设置为“name_desc”;否则,应将其设置为空字符串。 通过这两个语句,视图可如下设置列标题超链接:
当前排序顺序 |
姓氏超链接 |
日期超链接 |
---|---|---|
姓氏升序 | descending | ascending |
姓氏降序 | ascending | ascending |
日期升序 | ascending | descending |
日期降序 | ascending | ascending |
该方法使用 LINQ to Entities 指定要作为排序依据的列。 该代码在 switch 语句之前创建一个 IQueryable
变量,在 switch 语句中对其进行修改,并在 switch
语句后调用 ToListAsync
方法。 当创建和修改 IQueryable
变量时,不会向数据库发送任何查询。 只有通过调用 ToListAsync
之类的方法将 IQueryable
对象转换为集合,查询才会执行。 因此,此代码导致直到 return View
语句才会执行单个查询。
向“学生索引”视图添加列标题超链接
将 Views / Students / Index.cshtml 中的代码替换为以下代码,以添加列标题超链接。已更改的行为突出显示状态。
1 @model IEnumerable<ContosoUniversity.Models.Student> 2 3 @{ 4 ViewData["Title"] = "Index"; 5 } 6 7 <h2>Index</h2> 8 9 <p> 10 <a asp-action="Create">Create New</a> 11 </p> 12 <table class="table"> 13 <thead> 14 <tr> 15 <th> 16 <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a> 17 </th> 18 <th> 19 @Html.DisplayNameFor(model => model.FirstMidName) 20 </th> 21 <th> 22 <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a> 23 </th> 24 <th></th> 25 </tr> 26 </thead> 27 <tbody> 28 @foreach (var item in Model) { 29 <tr> 30 <td> 31 @Html.DisplayFor(modelItem => item.LastName) 32 </td> 33 <td> 34 @Html.DisplayFor(modelItem => item.FirstMidName) 35 </td> 36 <td> 37 @Html.DisplayFor(modelItem => item.EnrollmentDate) 38 </td> 39 <td> 40 <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> | 41 <a asp-action="Details" asp-route-id="@item.ID">Details</a> | 42 <a asp-action="Delete" asp-route-id="@item.ID">Delete</a> 43 </td> 44 </tr> 45 } 46 </tbody> 47 </table>
此代码使用 ViewData
属性中的信息来设置具有相应查询字符串值的超链接。
运行应用,选择“学生”选项卡,然后单击“姓氏”和“注册日期”列标题以验证排序是否正常工作。
向“学生索引”页添加搜索框
要向学生索引页添加筛选功能,需将文本框和提交按钮添加到视图,并在 Index
方法中做出相应的更改。 在文本框中输入一个字符串以在名字和姓氏字段中进行搜索。
向 Index 方法添加筛选功能
在 StudentsController.cs 中,将 Index
方法替换为以下代码(所做的更改为突出显示状态)。
1 public async Task<IActionResult> Index(string sortOrder, string searchString) 2 { 3 ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; 4 ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date"; 5 ViewData["CurrentFilter"] = searchString; 6 7 var students = from s in _context.Students 8 select s; 9 if (!String.IsNullOrEmpty(searchString)) 10 { 11 students = students.Where(s => s.LastName.Contains(searchString) 12 || s.FirstMidName.Contains(searchString)); 13 } 14 switch (sortOrder) 15 { 16 case "name_desc": 17 students = students.OrderByDescending(s => s.LastName); 18 break; 19 case "Date": 20 students = students.OrderBy(s => s.EnrollmentDate); 21 break; 22 case "date_desc": 23 students = students.OrderByDescending(s => s.EnrollmentDate); 24 break; 25 default: 26 students = students.OrderBy(s => s.LastName); 27 break; 28 } 29 return View(await students.AsNoTracking().ToListAsync()); 30 }
已向 Index
方法添加 searchString
参数。 从要添加到索引视图的文本框中接收搜索字符串值。 并且,还向 LINQ 语句添加了 where 子句,该子句仅选择名字或姓氏中包含搜索字符串的学生。 只有在有搜索值的情况下,才会执行添加了 where 子句的语句。
向“学生索引”视图添加搜索框
在 Views/Student/Index.cshtml 中,请在打开表格标签之前立即添加突出显示的代码,以创建标题栏、文本框和搜索按钮。
1 <p> 2 <a asp-action="Create">Create New</a> 3 </p> 4 5 <form asp-action="Index" method="get"> 6 <div class="form-actions no-color"> 7 <p> 8 Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" /> 9 <input type="submit" value="Search" class="btn btn-default" /> | 10 <a asp-action="Index">Back to Full List</a> 11 </p> 12 </div> 13 </form> 14 15 <table class="table">
此代码使用 <form>
标记帮助器添加搜索文本框和按钮。 默认情况下,<form>
标记帮助器使用 POST 提交表单数据,这意味着参数在 HTTP 消息正文中传递,而不是作为查询字符串在 URL 中传递。 当指定 HTTP GET 时,表单数据作为查询字符串在 URL 中传递,从而使用户能够将 URL 加入书签。 W3C 指南建议在操作不会导致更新时使用 GET。
运行应用,选择“学生”选项卡,输入搜索字符串,然后单击“搜索”以验证筛选是否正常工作。
注意,该 URL 包含搜索字符串。
http://localhost:5813/Students?SearchString=an
如果将此页加入书签,当使用书签时,将获得已筛选的列表。 向 form
标记添加 method="get"
是导致生成查询字符串的原因。
在此阶段,如果单击列标题排序链接,则会丢失已在“搜索”框中输入的筛选器值。 此问题将在下一部分得以解决。
向“学生索引”页添加分页功能
要向学生索引页添加分页功能,需创建一个使用 Skip
和 Take
语句的 PaginatedList
类来筛选服务器上的数据,而不总是对表中的所有行进行检索。 然后在 Index
方法中进行其他更改,并将分页按钮添加到 Index
视图。
在项目文件夹中,创建 PaginatedList.cs
,然后用以下代码替换模板代码。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Threading.Tasks; 5 using Microsoft.EntityFrameworkCore; 6 7 namespace ContosoUniversity 8 { 9 public class PaginatedList<T> : List<T> 10 { 11 public int PageIndex { get; private set; } 12 public int TotalPages { get; private set; } 13 14 public PaginatedList(List<T> items, int count, int pageIndex, int pageSize) 15 { 16 PageIndex = pageIndex; 17 TotalPages = (int)Math.Ceiling(count / (double)pageSize); 18 19 this.AddRange(items); 20 } 21 22 public bool HasPreviousPage 23 { 24 get 25 { 26 return (PageIndex > 1); 27 } 28 } 29 30 public bool HasNextPage 31 { 32 get 33 { 34 return (PageIndex < TotalPages); 35 } 36 } 37 38 public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize) 39 { 40 var count = await source.CountAsync(); 41 var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); 42 return new PaginatedList<T>(items, count, pageIndex, pageSize); 43 } 44 } 45 }
此代码中的 CreateAsync
方法将提取页面大小和页码,并将相应的 Skip
和 Take
语句应用于 IQueryable
。 当在 IQueryable
上调用 ToListAsync
时,它将返回仅包含请求页的列表。 属性 HasPreviousPage
和 HasNextPage
可用于启用或禁用“上一页”和“下一页”的分页按钮。
由于构造函数不能运行异步代码,因此使用 CreateAsync
方法来创建 PaginatedList<T>
对象,而非构造函数。
向 Index 方法添加分页功能
在 StudentsController.cs 中,将 Index
方法替换为以下代码。
1 public async Task<IActionResult> Index( 2 string sortOrder, 3 string currentFilter, 4 string searchString, 5 int? page) 6 { 7 ViewData["CurrentSort"] = sortOrder; 8 ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : ""; 9 ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date"; 10 11 if (searchString != null) 12 { 13 page = 1; 14 } 15 else 16 { 17 searchString = currentFilter; 18 } 19 20 ViewData["CurrentFilter"] = searchString; 21 22 var students = from s in _context.Students 23 select s; 24 if (!String.IsNullOrEmpty(searchString)) 25 { 26 students = students.Where(s => s.LastName.Contains(searchString) 27 || s.FirstMidName.Contains(searchString)); 28 } 29 switch (sortOrder) 30 { 31 case "name_desc": 32 students = students.OrderByDescending(s => s.LastName); 33 break; 34 case "Date": 35 students = students.OrderBy(s => s.EnrollmentDate); 36 break; 37 case "date_desc": 38 students = students.OrderByDescending(s => s.EnrollmentDate); 39 break; 40 default: 41 students = students.OrderBy(s => s.LastName); 42 break; 43 } 44 45 int pageSize = 3; 46 return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize)); 47 }
该代码向方法签名中添加一个页码参数、一个当前排序顺序参数和一个当前筛选器参数。
第一次显示页面时,或者如果用户没有单击分页或排序链接,所有参数都将为 NULL。 如果单击了分页链接,页面变量将包含要显示的页码。
名为 CurrentSort 的 ViewData
元素为视图提供当前排序顺序,因为此值必须包含在分页链接中,以便在分页时保持排序顺序相同。
名为 CurrentFilter 的 ViewData
元素为视图提供当前筛选器字符串。 此值必须包含在分页链接中,以便在分页过程中保持筛选器设置,并且在页面重新显示时必须将其还原到文本框中。
如果在分页过程中搜索字符串发生变化,则页面必须重置为 1,因为新的筛选器会导致显示不同的数据。 在文本框中输入值并按下“提交”按钮时,搜索字符串将被更改。 在这种情况下,searchString
参数不为 NULL。
if (searchString != null) { page = 1; } else { searchString = currentFilter; }
在 Index
方法最后,PaginatedList.CreateAsync
方法会将学生查询转换为支持分页的集合类型中的学生的单个页面。 然后将学生的单个页面传递给视图。
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
PaginatedList.CreateAsync
方法需要一个页码。 两个问号表示 NULL 合并运算符。 NULL 合并运算符为可为 NULL 的类型定义默认值;表达式 (page ?? 1)
表示如果 page
有值,则返回该值,如果 page
为 NULL,则返回 1。
向学生索引视图添加分页链接
在 Views/Students/Index.cshtml 中,将现有代码替换为以下代码。 突出显示所作更改。
1 @model PaginatedList<ContosoUniversity.Models.Student> 2 3 @{ 4 ViewData["Title"] = "Index"; 5 } 6 7 <h2>Index</h2> 8 9 <p> 10 <a asp-action="Create">Create New</a> 11 </p> 12 13 <form asp-action="Index" method="get"> 14 <div class="form-actions no-color"> 15 <p> 16 Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" /> 17 <input type="submit" value="Search" class="btn btn-default" /> | 18 <a asp-action="Index">Back to Full List</a> 19 </p> 20 </div> 21 </form> 22 23 <table class="table"> 24 <thead> 25 <tr> 26 <th> 27 <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a> 28 </th> 29 <th> 30 First Name 31 </th> 32 <th> 33 <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a> 34 </th> 35 <th></th> 36 </tr> 37 </thead> 38 <tbody> 39 @foreach (var item in Model) 40 { 41 <tr> 42 <td> 43 @Html.DisplayFor(modelItem => item.LastName) 44 </td> 45 <td> 46 @Html.DisplayFor(modelItem => item.FirstMidName) 47 </td> 48 <td> 49 @Html.DisplayFor(modelItem => item.EnrollmentDate) 50 </td> 51 <td> 52 <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> | 53 <a asp-action="Details" asp-route-id="@item.ID">Details</a> | 54 <a asp-action="Delete" asp-route-id="@item.ID">Delete</a> 55 </td> 56 </tr> 57 } 58 </tbody> 59 </table> 60 61 @{ 62 var prevDisabled = !Model.HasPreviousPage ? "disabled" : ""; 63 var nextDisabled = !Model.HasNextPage ? "disabled" : ""; 64 } 65 66 <a asp-action="Index" 67 asp-route-sortOrder="@ViewData["CurrentSort"]" 68 asp-route-page="@(Model.PageIndex - 1)" 69 asp-route-currentFilter="@ViewData["CurrentFilter"]" 70 class="btn btn-default @prevDisabled"> 71 Previous 72 </a> 73 <a asp-action="Index" 74 asp-route-sortOrder="@ViewData["CurrentSort"]" 75 asp-route-page="@(Model.PageIndex + 1)" 76 asp-route-currentFilter="@ViewData["CurrentFilter"]" 77 class="btn btn-default @nextDisabled"> 78 Next 79 </a>
页面顶部的 @model
语句指定视图现在获取的是 PaginatedList<T>
对象,而不是 List<T>
对象。
列标题链接使用查询字符串向控制器传递当前搜索字符串,以便用户可以在筛选结果中进行排序:
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
分页按钮由标记帮助器显示:
<a asp-action="Index" asp-route-sortOrder="@ViewData["CurrentSort"]" asp-route-page="@(Model.PageIndex - 1)" asp-route-currentFilter="@ViewData["CurrentFilter"]" class="btn btn-default @prevDisabled"> Previous </a>
运行应用并转到“学生”页。单击不同排序顺序的分页链接,以确保分页正常工作。 然后输入一个搜索字符串并再次尝试分页,以验证分页也可以正确地进行排序和筛选。
创建显示学生统计信息的“关于”页
对于 Contoso 大学网站的“关于”页,将显示每个注册日期注册了多少名学生。 这需要对组进行分组和简单计算。 若要完成此操作,需要执行以下操作:
-
为需要传递给视图的数据创建一个视图模型类。
-
修改主控制器中的“关于”方法。
-
修改“关于”视图。
创建视图模型
在 Models 文件夹中创建一个 SchoolViewModels 文件夹。
在新文件夹中,添加一个类文件 EnrollmentDateGroup.cs,并用以下代码替换模板代码:
1 using System; 2 using System.ComponentModel.DataAnnotations; 3 4 namespace ContosoUniversity.Models.SchoolViewModels 5 { 6 public class EnrollmentDateGroup 7 { 8 [DataType(DataType.Date)] 9 public DateTime? EnrollmentDate { get; set; } 10 11 public int StudentCount { get; set; } 12 } 13 }
修改主控制器
在 HomeController.cs 中,使用文件顶部的语句添加以下内容:
using Microsoft.EntityFrameworkCore; using ContosoUniversity.Data; using ContosoUniversity.Models.SchoolViewModels;
在类的左大括号之后立即为数据库上下文添加一个类变量,并从 ASP.NET Core DI 获取上下文的实例:
public class HomeController : Controller { private readonly SchoolContext _context; public HomeController(SchoolContext context) { _context = context; }
将 About
方法替换为以下代码:
public async Task<ActionResult> About() { IQueryable<EnrollmentDateGroup> data = from student in _context.Students group student by student.EnrollmentDate into dateGroup select new EnrollmentDateGroup() { EnrollmentDate = dateGroup.Key, StudentCount = dateGroup.Count() }; return View(await data.AsNoTracking().ToListAsync()); }
LINQ 语句按注册日期对学生实体进行分组,计算每组中实体的数量,并将结果存储在 EnrollmentDateGroup
视图模型对象的集合中。
修改“关于”视图
将 Views/Home/About.cshtml 文件中的代码替换为以下代码:
1 @model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup> 2 3 @{ 4 ViewData["Title"] = "Student Body Statistics"; 5 } 6 7 <h2>Student Body Statistics</h2> 8 9 <table> 10 <tr> 11 <th> 12 Enrollment Date 13 </th> 14 <th> 15 Students 16 </th> 17 </tr> 18 19 @foreach (var item in Model) 20 { 21 <tr> 22 <td> 23 @Html.DisplayFor(modelItem => item.EnrollmentDate) 24 </td> 25 <td> 26 @item.StudentCount 27 </td> 28 </tr> 29 } 30 </table>
运行应用并转到“关于”页。 表格中会显示每个注册日期的学生计数。
总结
在本节中,你已了解了如何执行排序、筛选、分页和分组。