用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>

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

总结

在本节中,你已了解了如何执行排序、筛选、分页和分组。

 *****************************
 *** Keep learning and growing. ***
 *****************************

猜你喜欢

转载自www.cnblogs.com/gangle/p/9207325.html