ASP.NET Core MVC 和 Entity Framework Core 入門教程 - 排序、過濾、分頁、分組(三)

排序、過濾、分頁、分組

Contoso 大學示例 Web 應用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創(chuàng)建 ASP.NET Core 2.0 MVC Web 應用程序。 如欲了解更多本教程相關信息,請參閱 入門
在前面的教程,你實現(xiàn)了一組 Student 實體的基本 CRUD 頁面。 在本節(jié)中,您將向 Student 列表頁添加排序、 篩選和分頁功能, 還將創(chuàng)建一個進行簡單分組的頁面。
下圖顯示本節(jié)中將會完成的頁面。 用戶可以點擊列標題進行排序。 重復點擊列標題將排序在升序和降序之間切換。

image.png

將列排序鏈接添加到學生索引頁 (Student Index)

要在學生索引頁中添加排序,需要更改 Students 控制器中的 Index 的方法,并添加代碼到 Students 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 查詢參數(shù),此查詢參數(shù)由 ASP.NET Core MVC 提供。參數(shù)是值為 "Name" 或 "Date" 的字符串,有時候后面會帶有下劃線和字符串 "desc" 來指定降序順序。 默認排序順序為升序。
第一次請求索引頁時,沒有附加查詢字符串。 在默認的 Switch default 方法中按 LastName 排序。 當用戶單擊列標題,相應的 sortOrder 將會出現(xiàn)在查詢字符串中。
兩個 ViewData 元素 ( NameSortParm 和 DateSortParm )供視圖用于配置列標題超鏈接查詢字符串。

ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
當前排序情況 LastName 鏈接 Date 鏈接
LastName 升序 降序 升序
LastName 降序 升序 升序
Date 升序 升序 降序
Date 降序 升序 升序

這是三元選擇語句。 如果 sortOrder 參數(shù)為 null 或為空,NameSortParm 應設置為 "name_desc"; 否則,設置為一個空字符串。 這兩個語句在視圖中用于設置列標題超鏈接,如下所示:

當前排序情況 LastName 鏈接 Date 鏈接
LastName 升序 降序 升序
LastName 降序 升序 升序
Date 升序 升序 降序
Date 降序 升序 升序

方法中使用 LINQ to Entities 指定排序列。 在進行 Switch 判斷前, 創(chuàng)建 IQueryables 變量, 在判斷之后, 調(diào)用 ToListAsync 方法。 在創(chuàng)建和修改 IQueryable 變量過程中,查詢并不會真正發(fā)送到數(shù)據(jù)庫,直到你通過調(diào)用一個類似 ToListAsync 的方法將 IQueryable 變量轉(zhuǎn)化為一個集合。 因此,在這段代碼中,只當返回 View 語句執(zhí)行時,查詢才真正發(fā)生。
這樣的代碼可能導致出現(xiàn)非常多的列變量,在本系列最后一個教程中將告訴你如何在變量中傳遞排序列名。

在學生索引視圖中添加列標題超鏈接

為了添加列標題超鏈接,替換 Views/Students/Index.cshtml 文件中的代碼為如下代碼:

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

代碼使用 ViewData 屬性中的信息建立超鏈接中的查詢字符串。
運行應用程序中,選擇 Student 菜單,然后單擊 Last name 和 Enrollement Date 列標題,以驗證排序是否生效。


image.png

在學生索引視圖中添加搜索框

要在學生索引頁面中添加過濾功能,您需要在視圖中添加一個文本框和一個提交按鈕,并在 Index 方法中做相應修改。 文本框中,你將輸入要在名字和姓氏字段中搜索的字符串。

在 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 參數(shù),此參數(shù)值來自剛剛加入視圖中的文本框。同時,在 LINQ 語句中添加一個 Where 子句來選擇名字 (first name 和 last name)中包含查詢字符串的學生。Where 子句僅當查詢字符串中有值時才生效。

備注

在這里, 您在 IQueryable 對象上調(diào)用 Where 方法, 過濾將在服務器上進行。某些情況下,您也可能是對內(nèi)存集合調(diào)用 Where 方法。(例如,假設你將 _context.Students 的引用,從 EF Dataset 修改為一個返回 IEnumerable 的倉儲方法。)查詢結(jié)果通常是相同的,但在某些情況下可能會有所不同。
例如,.NET Framework 實現(xiàn)的 Contains 方法默認區(qū)分大小寫。但 SQL Server 中這取決于 SQL Server 實例的排序規(guī)則設置,該設置默認為不區(qū)分大小寫。 您可以調(diào)用 ToUpper 來進行測試顯式不區(qū)分大小寫的方法:Where (s = > s.LastName.ToUpper()。Contains(searchString.ToUpper())。 這將確保如果稍后你修改代碼為使用返回 IEnumerable 對象的倉儲 Repository,而不是返回 IQueryable 對象時,結(jié)果保持相同。 (當您在 IEnumerable 集合上調(diào)用 Contains 方法時,使用的是 .NET Framework 實現(xiàn); 而在 IQueryable 對象上,則使用 database provider 實現(xiàn)。) 但是,這個解決方案將對性能產(chǎn)生負面影響。 ToUpper 代碼將在 TSQL 查詢語句的 Where 條件中加入函數(shù)調(diào)用,進而導致 SQL 優(yōu)化器停止使用索引。 假設 SQL 主要安裝為不區(qū)分大小寫,最好是避免 ToUpper 代碼,直到您遷移到區(qū)分大小寫的數(shù)據(jù)存儲區(qū)。

在 Index 視圖中添加搜索框

在Views/Student/Index.cshtml,在 <Table> 標簽前加入如下代碼,創(chuàng)建一個標題、一個文本框和一個搜索按鈕。

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

代碼使用 <form> 標簽,添加搜索文本框和按鈕。默認情況下,<form> 標簽使用 POST 方法進行數(shù)據(jù)提交,參數(shù)在消息正文而不是 URL 查詢字符串中傳遞。通過指定使用 GET 方法,窗體數(shù)據(jù)通過 URL 查詢字符串進行傳遞,這是的用戶可以對 URL 創(chuàng)建書簽。 W3C 準則建議,在未導致更新的操作中,使用 GET 方法。
運行應用程序中,選擇 Student 菜單,輸入任意搜索字符,并點擊“搜索”按鈕,以驗證過濾功能生效。

image.png

請注意在 URL 中包含了搜索字符串。

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

如果您將本頁面加入書簽,下次使用書簽時,您將得到過濾后的列表。在 Form 標簽中添加的 method="get" 是產(chǎn)生查詢字符串的原因。
在此階段,如果您單擊列標題進行排序,你將丟失搜索框中輸入的過濾查詢。 在下一部分中將修復此問題。

在學生索引視圖中添加分頁功能

要在學生索引頁中添加分頁功能,您將創(chuàng)建一個 PaginatedList 類,在類中使用 SkipTake 語句實現(xiàn)在服務器過濾數(shù)據(jù),而不是獲取數(shù)據(jù)表的所有數(shù)據(jù)行。然后再對 Index 做一些更改,再 Index 視圖中添加分頁按鈕。下圖中展示了分頁按鈕。

image.png

在項目文件夾中,創(chuàng)建 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 上調(diào)用 ToListAsync 后, 返回一個只包含請求頁的列表。 屬性 HasPreviousPageHasNextPage 用于啟用或禁用 “上一頁” 和 “下一頁” 按鈕。
PaginatedList<T> 中使用 CreateAsync 方法而不是構(gòu)造函數(shù)的原因是構(gòu)造函數(shù)無法運行異步代碼。
ACreateAsync方法用于而不是一個構(gòu)造函數(shù)創(chuàng)建PaginatedList<T>對象,因為構(gòu)造函數(shù)不能運行異步代碼。

Index 方法中添加分頁功能

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));
}

代碼在方法中添加了 page, sortOrder, currentFilter 三個參數(shù)。
第一次顯示頁面,或如果用戶未單擊分頁或排序鏈接,則所有參數(shù)將都為 null。 單擊分頁鏈接時,如果頁變量將包含要顯示的頁碼。
ViewData("CurrentSort") 保存當前排序以供視圖使用。在視圖的分頁鏈接中包含排序,翻頁的時候才能保持排序不變。
ViewData("CurrentFilter")保存當前過濾字符串以供視圖使用。在視圖的分頁鏈接中包含過濾字符串,翻頁額時候才能保持過濾不變。
如果在分頁期間,搜索字符串被更改,因為新的過濾導致顯示不同的數(shù)據(jù),頁碼必須被重置為第一頁。在文本框中輸入并按下提交按鈕時,搜索字符串改變。在這種情況下,searchString 參數(shù)不為空。

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

Index 方法結(jié)尾, PaginatedList.CreateAsync 方法轉(zhuǎn)化學生查詢至一個支持分頁功能的單頁學生集合,然后這個集合被傳遞給視圖。

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

PaginatedList.CreateAsync 方法使用參數(shù) page (頁碼)和pageSize (頁大?。┳鳛閰?shù)。 page 參數(shù)后的兩個 ? 代表 null 合并運算符 。null 合并運算符 定義了可為空類型的默認值;page ?? 1 表達式意味著,如果 page 具有一個值(不為空),則返回 page, 如果為空則返回 1 。

Index 視圖中添加分頁鏈接

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>

譯者注:Markdown 語法無法實現(xiàn)代碼內(nèi)高亮,如不清楚修改的位置,請參考微軟原文。
頁面頂部的 @model 指定視圖現(xiàn)在獲取 PaginatedList<T> 對象而不是 List<T> 對象。
列標題上的鏈接使用查詢字符串將當前的搜索字符串傳遞到控制器,以便用戶可以在過濾后的結(jié)果中進行排序:

<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>

The paging buttons are displayed by tag helpers:
分頁按鈕使用 tag helpers 進行顯示

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

運行應用并轉(zhuǎn)到 Student 頁面。


image.png

在不同排序狀態(tài)下點擊分頁鏈接,以確認分頁功能正常工作。然后嘗試搜索后再分頁,驗證分頁功能在不同排序和過濾條件下都正常工作。

創(chuàng)建一個顯示學生統(tǒng)計信息的關于頁面

在 Contoso 大學網(wǎng)站的 About 頁面, 將顯示每天有多少學生進行注冊,這需要對數(shù)據(jù)進行分組,并在分組上做計算。要完成此任務,您需要執(zhí)行以下操作:

  • 創(chuàng)建一個用于傳遞數(shù)據(jù)到視圖的 ViewModel 類。
  • 修改 HomeController 中的 About 方法。
  • 修改 About 視圖。

創(chuàng)建 ViewModel 類

Models 文件夾中創(chuàng)建一個 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; }
    }
}

修改 HomeController

HomeController.cs 文件, 頂部加入如下語句:

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

在類中添加一個數(shù)據(jù)庫上下文變量 _context, 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 語句將 Student 實體進行分組,計算每個分組中的實體數(shù)量,并將結(jié)果存放在 EnrollmentDateGroup ViewModel 對象中。

備注

在 EF Core 1.0 版本中, 整個結(jié)果集返回到客戶端,并在客戶端上進行分組。在某些情況下,這會導致性能問題。請使用實際生產(chǎn)環(huán)境規(guī)模的數(shù)據(jù)測試性能,如有必要,使用原始 SQL 在服務器進行分組。 有關如何使用原始的 SQL 的信息,請參閱本系列最后一個教程。

修改 About 視圖

替換 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>

運行應用,轉(zhuǎn)至 About 頁面。 每個日期的學生注冊數(shù)量顯示于表格中。


image.png

小結(jié)

在本教程中,你已了解如何執(zhí)行排序、 篩選、 分頁和分組。 在下一步的教程中,你將了解如何通過使用遷移來處理數(shù)據(jù)模型更改。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容