ASP.NET Core 和 EF Core 系列教程——排序、筛选、分页和分组

ASP.NET Core 和 EF Core 系列教程——排序、筛选、分页和分组(3 / 10)

作者:Tom DykstraRick Anderson

Contoso 大学示例 web 应用程序演示如何使用 Entity Framework Core 和 Visual Studio 创建 ASP.NET Core MVC web 应用程序。 想要获取有关系列教程的信息,请参阅第一个教程

在前面的教程,你可以实现一组的用于学生实体的基本 CRUD 操作网页。 在本教程将向学生索引页添加排序、 筛选和分页功能。 你还将创建具有简单分组功能的页面。

下图显示你完成本教程后相关页面的样子。 列标题时一个链接,用户可以单击它使数据按该列排序。 反复单击列标题在升序排列和降序排列之间切换。

学生索引页

将列排序链接添加到学生索引页

为了添加排序学生索引页,你将更改学生控制器中的Index方法并向学生索引视图添加相关的代码。

向 Index 方法添加排序功能

StudentsController.cs,用以下代码替换Index方法:

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

此代码从 URL 中的查询字符串中接收sortOrder参数。 ASP.NET Core MVC 提供的查询字符串作为参数传递给的操作方法。 “Name”或”Date”,后面可以选择性跟用于指定降序顺序的下划线和”desc”构成参数字符串。 默认排序顺序为升序。

第一次请求索引页时,没有任何查询字符串。 学生按姓氏升序显示也就是switch语句中的缺省值中的排序方式。 当用户单击列标题的超链接,将向Index方法提供相应的sortOrder查询字符串。

视图使用ViewData元素中两个元素 (NameSortParm 和 DateSortParm) 对应的查询字符串值配置列标题超链接。

public async Task<IActionResult> Index(string sortOrder)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    var students = from s in _context.Students
                   select s;
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

这两个语句都使用了三目运算符。 第一个语句指如果sortOrder参数为 null 或为空则 NameSortParm 应设置为”name_desc”; 否则,它应设置为一个空字符串。 这两个语句使试图能够如下所示设置列标题的超链接:

当前的排序顺序Last Name 超链接Date 超链接
Last Name 升序排列descendingascending
Last Name 降序排列ascendingascending
Date 升序排列ascendingdescending
Date 降序排列ascendingascending

该方法使用 LINQ to Entities 指定要作为排序依据的列。 代码在switch 语句之前创建了IQueryable变量然后在 switch 语句中对其进行修改,并在switch语句之后调用ToListAsync方法。 当你创建和修改IQueryable变量时数据库不会接收到任何查询。 在您调用如ToListAsync等方法将IQueryable转换为集合对象之前不会执行查询。 因此,在return View语句之前此代码只会执行一个查询。

此代码会获得具有大量列的冗长信息。 本系列最后一个教程将演示如何编写代码,使你可以使用字符串将需要OrderBy的行的名称作为参数传递给方法。

向学生索引视图的列标题添加超链接

用以下代码替换Views/Students/Index.cshtml,以添加列标题超链接。 高亮代码为已更改的行。

@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

代码中使用了ViewData元素中的信息来以相应的查询字符串值设置超链接。

运行应用程序中,选择Students卡,然后单击Last NameEnrollment Date列标题,以验证该排序成功。

名称顺序中的学生索引页

向学生索引页添加搜索框

向视图添加一个文本框和提交按钮来向索引页添加搜索框,并在Index方法中做相应更改。 你可以在文本框中输入字符串搜索名字和姓氏字段中的内容。

向索引方法添加筛选功能

StudentsController.cs,将Index方法替换为以下代码 (突出显示所做的更改)。

public async Task<IActionResult> Index(string sortOrder, string searchString)
{
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }
    return View(await students.AsNoTracking().ToListAsync());
}

Index方法中添加searchString参数。 搜索字符串值来自之后会添加到索引视图中的文本框。 你还向 LINQ 语句 添加了where 子句来选择仅名字或姓氏包含搜索字符串的学生。 添加 where 子句的语句只有在要搜索值的时候才执行。

此处对IQueryable对象调用Where方法,筛选器将在服务器上处理。 在某些场景下你可能会对内存中集合调用作为扩展方法的Where。 (例如,假设你使用_context.Students引用,不同于 EFDbSet,它返回IEnumerable集合的存储库方法的引用。)结果通常将相同,但在某些情况下可能会不同。

例如,默认情况下 .NET Framework 实现的Contains方法是对大小写敏感的,但 SQL Server 中由 SQL Server 的排序规则确定。 SQL Server 默认不区分大小写。 您可以调用ToUpper方法使得其大小写敏感:Where (s = > s.LastName.ToUpper()。Contains(searchString.ToUpper())。 这样做能确保如果将来使用返回IEnumerable集合的存储库方法而不是IQueryable对象来修改相关代码,结果还能保持相同。 (当你对IEnumerable集合调用Contains方法,你将获取.NET Framework 的实现; 当对IQueryable对象调用它,则会得到数据库驱动的实现。)但是,此解决方案会对性能产生负面影响。 ToUpper将函数加入到 TSQL SELECT 语句的 WHERE 子句中。 这样做会是的索引优化失去效果。 假设 SQL 大多是是大小写不敏感,在你将数据迁移到大小写敏感的数据存储库之前最好避免ToUpper代码。

向学生索引视图添加一个搜索框

打开Views/Student/Index.cshtml,在table标签之前添加高亮代码以在页面中创建标题,Search文本框,按钮。

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">

此代码通过使用<form>标记帮助器添加搜索文本框和按钮。 默认情况下,<form>标记帮助器默认使用 post 方法提交数据,这意味着,参数在 HTTP 消息正文中传输表单数据,而不是在 URL 查询字符串上显示并传输。指定使用 HTTP GET 时,表单数据是通过 URL 查询字符串传输,这使得用户能够使用该 URL 来创建书签。 W3C 指南建议当操作未导致更新时使用 GET 方法。

运行应用程序,选择Students选项卡,输入搜索字符串,然后单击搜索以验证筛选是否正常工作。

使用筛选的学生索引页

请注意该 URL 包含搜索字符串。

http://localhost:5813/Students?SearchString=an

如果将此页加入书签,使用书签时你将获得筛选后的列表。 添加method="get"form标签中是导致生成查询字符串的原因。

在此阶段,如果您单击列标题的排序链接你将丢失url上的查询字符串值。 下一部分将修复此问题。

向学生索引页添加分页功能

为了向学生索引页添加分页功能,你需要创建PaginatedList类,该类使用SkipTake语句来对服务器上的数据进行筛选而不是始终检索所有的表行。 接下来你将对Index方法做更多的修改并将分页按钮添加到Index视图中。 如下图所示添加了分页按钮的学生索引页。

学生索引页,带有分页链接

在项目文件夹中,创建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方法获得页面数和当前页码,并对IQueryable执行 相应的SkipTake语句。 当IQueryable调用ToListAsync时,该方法将返回只包含在请求页里的学生列表。 属性HasPreviousPageHasNextPage可用来启用或禁用PreviousNext分页按钮。

由于构造函数里不能运行异步代码,CreateAsync方法被用作构造函数而只用于创建一个PaginatedList<T>对象。

向索引方法添加分页功能

StudentsController.cs中,用以下代码替换Index方法。

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? page)
{
    ViewData["CurrentSort"] = sortOrder;
    ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";

    if (searchString != null)
    {
        page = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    ViewData["CurrentFilter"] = searchString;

    var students = from s in _context.Students
                   select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        students = students.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            students = students.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            students = students.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            students = students.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            students = students.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
}

代码中将总页数参数、 当前的排序顺序参数和当前的筛选器参数添加到方法签名中。

public async Task<IActionResult> Index(
    string sortOrder,
    string currentFilter,
    string searchString,
    int? page)

第一次显示页面,或如果用户未单击分页或排序链接,则所有参数都为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参数为 null 则返回 1,如果指定了一个值则返回指定的值。

向学生索引视图添加分页链接

Views/Students/Index.cshtml中,用以下代码替换现有代码。 高亮代码为更改的代码。

@model PaginatedList<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-action="Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@ViewData["currentFilter"]" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-action="Index">Back to Full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
            </th>
            <th>
                First Name
            </th>
            <th>
                <a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <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-action="Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}

<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>
<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-page="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</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 大学网站About页上,将显示每个课程有多少学生修读。 这要求在分组上再进行分组和简单计算。 要完成此操作,需要执行以下操作:

  • 创建一个视图模型类,该视图类是需要传递到该视图的数据的抽象。

  • 修改对 Home 控制器中的 About 方法。

  • 修改关于视图。

创建视图模型

Model文件夹中创建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; }
    }
}

修改 Home 控制器

HomeController.cs,在该文件的顶部添加以下 using 语句:

using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;

在类中,左左大括号后添加的数据库上下文类型的变量,并通过 ASP.NET Core 依赖注入获取上下文的实例:

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

在 Entity Framework Core 1.0 版本中,整个结果集都会返回到客户端,并在客户端上进行分组。 在某些场景下,这会导致性能问题。 请务必使用用符合生产规模的数据来测试性能,如有必要使用原始 SQL 语句在服务器上进行分组。 有关如何使用原始的 SQL ,请参阅本系列最后一个教程

修改关于视图

Views/Home/About.cshtml文件替换为以下代码:

@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>

@{
    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)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

运行应用并转到关于页面。 表格中显示了每个修读日期的学生计数。

有关页面

摘要

在本教程中,你已了解如何执行排序、 筛选、 分页和分组。 在下一个的教程中,你将了解如何使用迁移来处理数据模型更改。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值