第 3 部分,在 ASP.NET Core 中具有 EF Core 的 Razor Pages - 排序、篩選、分頁

作者:Tom DykstraJeremy LiknessJon P Smith

Contoso 大學 Web 應用程式將示範如何使用 EF Core 和 Visual Studio 來建立 Razor Pages Web 應用程式。 如需教學課程系列的資訊,請參閱第一個教學課程

如果您遇到無法解決的問題,請下載已完成的應用程式,並遵循本教學課程以將程式碼與您所建立的內容進行比較。

本教學課程會將排序、篩選和分頁功能新增至 Students 頁面。

下圖顯示已完成的頁面。 資料行標題為可按式連結,可用以排序資料行。 重覆按一下資料行標題,可在遞增和遞減排序次序之間切換。

Students index page

新增排序

以下列程式碼取代 Pages/Students/Index.cshtml.cs 中的程式碼來新增排序。

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)
    {
        // using System;
        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();
    }
}

上述 程式碼:

  • 需要新增 using System;
  • 新增屬性來包含排序參數。
  • Student 屬性的名稱變更為 Students
  • 取代 OnGetAsync 方法中的程式碼。

OnGetAsync 方法會從 URL 中的查詢字串接收 sortOrder 參數。 URL 和查詢字串是由錨點標籤協助程式所產生。

sortOrder 參數為 NameDatesortOrder 參數後面可以選擇接著 _desc 來指定遞減順序。 預設排序順序為遞增。

Students 連結要求 [索引] 頁面時,將不會有查詢字串。 學生會以遞增姓氏順序顯示。 依姓氏遞增順序是 switch 陳述式中的 default。 使用者按一下資料行標題連結時,適當的 sortOrder 值將會在查詢字串值中提供。

Razor Page 會以適當的查詢字串值,使用 NameSortDateSort 來設定資料行標題超連結:

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

程式碼會使用 C# 條件運算子 ?:?: 運算子是一種三元運算子, 其會採用三個運算元。 第一行指定當 sortOrder 為 null 或空白時,NameSort 設為 name_desc。 如果 sortOrder不是 null 或空白,則 NameSort 設為空字串。

這兩個陳述式讓頁面能夠設定資料行標題超連結,如下所示:

目前排序次序 姓氏超連結 日期超連結
姓氏遞增 遞減 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 來簡化程式碼

以下列程式碼取代 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>

上述 程式碼:

  • 將超連結新增至 LastNameEnrollmentDate 資料行標題。
  • 使用 NameSortDateSort 的資訊,以目前排序次序值來設定超連結。
  • 將頁面標題從索引變更為 Students。
  • Model.Student 變更為 Model.Students

若要確認該排序運作正常:

  • 執行應用程式並選取 [Students] 索引標籤。
  • 按一下資料行標題。

新增篩選

將篩選新增至 Students [索引] 頁面:

  • 文字輸入框和提交按鈕會新增至 Razor Page。 文字方塊提供名字或姓氏的搜尋字串。
  • 頁面模型會更新為使用文字方塊的值。

更新 OnGetAsync 方法

以下列程式碼取代 Students/Index.cshtml.cs 中的程式碼來新增篩選:

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 屬性中。 從文字方塊中接收搜尋字串值文字方塊會在下一節新增。
  • Where 子句新增至 LINQ 陳述式。 Where 子句只會選取名字或姓氏包含搜尋字串的學生。 有可以搜尋的值,LINQ 陳述式才會執行。

IQueryable 與IEnumerable

程式碼會呼叫 IQueryable 物件上的 Where 方法,且會在伺服器上處理篩選。 在某些情況下,應用程式可能會呼叫 Where 方法在記憶體內部集合上作為擴充方法。 例如,假設 _context.Students 從 EF CoreDbSet 變更為傳回 IEnumerable 集合的存放庫方法。 結果通常都是一樣的,但在某些情況下可能會不同。

例如,.NET Framework 的 Contains 實作,預設會執行區分大小寫的比較。 在 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 Page

取代 Pages/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">
                    @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。

測試應用程式:

  • 選取 [Students] 索引標籤並輸入搜尋字串。 如果您使用 SQLite,則只有在您實作先前示範的選擇性 ToUpper 程式碼時,篩選才會不區分大小寫。

  • 選取搜尋

請注意 URL 中包含了搜尋字串。 例如:

https://localhost:5001/Students?SearchString=an

如果頁面已加上書籤,那麼書籤會包含該頁面的 URL 和 SearchString 查詢字串。 form 中的 method="get" 導致查詢字串的產生。

目前,選取資料行標題排序連結時,[搜尋] 方塊中的篩選值將會遺失。 遺失的篩選值會在下一節修正。

新增分頁

在本節中,PaginatedList 類別用來支援分頁。 PaginatedList 類別會使用 SkipTake 陳述式來篩選伺服器上的資料,而不會擷取資料表中的所有資料列。 下圖顯示分頁按鈕。

Students index page with paging links

建立 PaginatedList 類別

在專案資料夾中,以下列程式碼建立 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 => PageIndex > 1;

        public bool HasNextPage => 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 方法會採用頁面大小和頁面數,並會將適當的 SkipTake 陳述式套用至 IQueryable。 在 IQueryable 上呼叫 ToListAsync 時,會傳回僅包含所要求頁面的清單。 HasPreviousPageHasNextPage 屬性可用來啟用或停用 PreviousNext 分頁按鈕。

CreateAsync 方法為建立 PaginatedList<T> 之用。 建構函式無法建立 PaginatedList<T> 物件,建構函式也無法執行非同步程式碼。

新增頁面大小至設定

新增 PageSizeappsettings.json設定檔

{
  "PageSize": 3,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

新增分頁至 IndexModel

取代 Students/Index.cshtml.cs 中的程式碼以新增分頁。

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly SchoolContext _context;
        private readonly IConfiguration Configuration;

        public IndexModel(SchoolContext context, IConfiguration configuration)
        {
            _context = context;
            Configuration = configuration;
        }

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

            var pageSize = Configuration.GetValue("PageSize", 4);
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}

上述 程式碼:

  • Students 屬性的型別從 IList<Student> 變更為 PaginatedList<Student>
  • 將頁面索引、目前的 sortOrdercurrentFilter 新增至 OnGetAsync 方法簽章。
  • 儲存 CurrentSort 屬性中的排列順序。
  • 當有新的搜尋字串時,將頁面索引重設為 1。
  • 會使用 PaginatedList 類別來取得 Students 實體。
  • 設定中將 pageSize 設定為 3,如果設定失敗,則為 4。

遇下列情況時,OnGetAsync 接收的所有參數都會成為 Null:

  • Students 連結呼叫頁面。
  • 使用者沒有按一下分頁或排序連結。

按一下分頁連結時,頁面索引變數即包含要顯示的頁面數。

CurrentSort 屬性提供 Razor Page 目前的排列順序。 目前的排序次序必須包含在分頁連結中,以保留分頁時的排序次序。

CurrentFilter 屬性提供 Razor Page 目前的篩選字串。 CurrentFilter 值:

  • 必須包含在分頁連結中,以保留分頁時的篩選設定。
  • 頁面重新顯示時,必須還原到文字方塊中。

如果搜尋字串在分頁時變更,頁面會重設為 1。 頁面必須重設為 1 是因為新的篩選可能會導致顯示不同的資料。 輸入搜尋值和選取 Submit 時:

  • 搜尋字串變更。
  • searchString 參數不是 null。

PaginatedList.CreateAsync 方法會以支援分頁的集合類型,將學生查詢轉換成學生單一頁面。 該單一頁面的學生會傳遞至 Razor Page。

PaginatedList.CreateAsync 呼叫中,pageIndex 之後的兩個問號代表 Null 聯合運算子。 Null 聯合運算子會針對可為 Null 的型別定義一個預設值。 如果運算式有值,則運算式 pageIndex ?? 1 會傳回 pageIndex 的值,否則會傳回 1。

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>

執行應用程式並巡覽至學生頁面。

  • 若要確定分頁運作正常,請以不同排序次序按一下分頁連結。
  • 若要驗證分頁的排序和篩選能正確運作,請輸入搜尋字串並嘗試分頁。

students index page with paging links

群組

本節將建立 [About] 頁面,其中會顯示每個註冊日期的已註冊學生人數。 此更新會使用群組,並包含下列步驟:

  • 建立資料的檢視模型以用於 [About] 頁面。
  • 更新 [About] 頁面以使用檢視模型。

建立檢視模型

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

建立頁面模型

以下列程式碼更新 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 檢視模型物件的集合中。

執行應用程式並巡覽至 About 頁面。 每個註冊日期的學生人數將會顯示在資料表中。

About page

下一步

在下一個教學課程中,應用程式將會使用移轉來更新資料模型。

在本教學課程中,將新增排序、篩選、分組和分頁功能。

下圖顯示已完成的頁面。 資料行標題為可按式連結,可用以排序資料行。 重覆按一下資料行標題,可切換遞增和遞減排序次序。

Students 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參數為 "Name" 或 "Date"。sortOrder 參數後面可以選擇接著 "_desc" 來指定遞減順序。 預設排序順序為遞增。

Students 連結要求 [索引] 頁面時,將不會有查詢字串。 學生會以遞增姓氏順序顯示。 在 switch 陳述式中,預設會依姓氏遞增排序 (fall-through 大小寫)。 使用者按一下資料行標題連結時,適當的 sortOrder 值將會在查詢字串值中提供。

Razor Page 會以適當的查詢字串值,使用 NameSortDateSort 來設定資料行標題超連結:

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 也是所謂的三元運算子。

這兩個陳述式讓頁面能夠設定資料行標題超連結,如下所示:

目前排序次序 姓氏超連結 日期超連結
姓氏遞增 遞減 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 可取得使用大量可排序資料行數的詳細資訊。

以下列醒目提示的程式碼取代 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>

上述 程式碼:

  • 將超連結新增至 LastNameEnrollmentDate 資料行標題。
  • 使用 NameSortDateSort 的資訊,以目前排序次序值來設定超連結。

若要確認該排序運作正常:

  • 執行應用程式並選取 [Students] 索引標籤。
  • 按一下 [姓氏]
  • 按一下 [註冊日期]

若要更深入了解這個程式碼:

  • Students/Index.cshtml.cs,在 switch (sortOrder) 上設定中斷點。
  • NameSortDateSort 新增監看式。
  • Students/Index.cshtml,在 @Html.DisplayNameFor(model => model.Student[0].LastName) 上設定中斷點。

逐步執行偵錯工具。

將搜尋方塊新增至 Students [索引] 頁面

將篩選新增至 Students [索引] 頁面:

  • 文字輸入框和提交按鈕會新增至 Razor Page。 文字方塊提供名字或姓氏的搜尋字串。
  • 頁面模型會更新為使用文字方塊的值。

將篩選功能新增至 Index 方法

使用下列程式碼更新 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();
}

上述 程式碼:

  • searchString 參數新增至 OnGetAsync 方法。 從文字方塊中接收搜尋字串值文字方塊會在下一節新增。
  • Where 子句新增至 LINQ 陳述式。 Where 子句只會選取名字或姓氏包含搜尋字串的學生。 有可以搜尋的值,LINQ 陳述式才會執行。

注意:上述程式碼會在 IQueryable 物件上呼叫 Where 方法,而篩選是由伺服器處理。 在某些情況下,應用程式可能會呼叫 Where 方法在記憶體內部集合上作為擴充方法。 例如,假設 _context.Students 從 EF CoreDbSet 變更為傳回 IEnumerable 集合的存放庫方法。 結果通常都是一樣的,但在某些情況下可能會不同。

例如,.NET Framework 的 Contains 實作,預設會執行區分大小寫的比較。 在 SQL Server,Contains 區分大小寫取決於 SQL Server 執行個體的定序設定。 SQL Server 預設為不區分大小寫。 可以使用 ToUpper 使測試明確不區分大小寫:

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

如果程式碼變更為使用 IEnumerable,則上述程式碼會確保結果不區分大小寫。 在 IEnumerable 集合上呼叫 Contains 時,會使用 .NET Core 實作。 在 IQueryable 物件上呼叫 Contains 時,會使用資料庫實作。 從存放庫傳回 IEnumerable 可能對效能產生明顯的負面影響:

  1. 所有列都會從資料庫伺服器傳回。
  2. 在應用程式中,傳回的所有資料列都會套用篩選。

呼叫 ToUpper 會使效能降低。 ToUpper 程式碼會將一個函式新增至 TSQL SELECT 陳述式的 WHERE 子句中。 新增的函式會防止最佳化工具使用索引。 假如 SQL 已安裝為不區分大小寫,除非有需要,否則應盡量避免呼叫 ToUpper

將搜尋方塊新增至學生的 [索引] 頁面

Pages/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">

上述程式碼會使用 <form>標籤協助程式 新增搜尋文字方塊和按鈕。 <form> 標籤協助程式預設為使用 POST 提交表單資料。 在 POST 中,參數會在 HTTP 訊息本文中傳遞,而不是在 URL 中傳遞。 使用 HTTP GET 時,表單資料會以查詢字串的形式在 URL 中傳遞。 以查詢字串來傳遞資料,可讓使用者為 URL 加上書籤。 W3C 指導方針 建議,只有在動作不會產生更新時才應使用 GET。

測試應用程式:

  • 選取 [Students] 索引標籤並輸入搜尋字串。
  • 選取搜尋

請注意 URL 中包含了搜尋字串。

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

如果頁面已加上書籤,那麼書籤會包含該頁面的 URL 和 SearchString 查詢字串。 form 中的 method="get" 導致查詢字串的產生。

目前,選取資料行標題排序連結時,[搜尋] 方塊中的篩選值將會遺失。 遺失的篩選值會在下一節修正。

將分頁功能新增至 Students 的 [索引] 頁面

在本節中,PaginatedList 類別用來支援分頁。 PaginatedList 類別會使用 SkipTake 陳述式來篩選伺服器上的資料,而不會擷取資料表中的所有資料列。 下圖顯示分頁按鈕。

Students index page with paging links

在專案資料夾中,以下列程式碼建立 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 => PageIndex > 1;

        public bool HasNextPage => 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 方法會採用頁面大小和頁面數,並會將適當的 SkipTake 陳述式套用至 IQueryable。 在 IQueryable 上呼叫 ToListAsync 時,會傳回僅包含所要求頁面的清單。 HasPreviousPageHasNextPage 屬性可用來啟用或停用 PreviousNext 分頁按鈕。

CreateAsync 方法為建立 PaginatedList<T> 之用。 建構函式無法建立 PaginatedList<T> 物件,建構函式也無法執行非同步程式碼。

將分頁功能新增至 Index 方法

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

上述程式碼將頁面索引、目前的 sortOrdercurrentFilter 新增至方法簽章。

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)

遇下列情況時,所有的參數都會成為 null:

  • Students 連結呼叫頁面。
  • 使用者沒有按一下分頁或排序連結。

按一下分頁連結時,頁面索引變數即包含要顯示的頁面數。

CurrentSort 提供 Razor Page 目前的排列順序。 目前的排序次序必須包含在分頁連結中,以保留分頁時的排序次序。

CurrentFilter 提供 Razor Page 目前的篩選字串。 CurrentFilter 值:

  • 必須包含在分頁連結中,以保留分頁時的篩選設定。
  • 頁面重新顯示時,必須還原到文字方塊中。

如果搜尋字串在分頁時變更,頁面會重設為 1。 頁面必須重設為 1 是因為新的篩選可能會導致顯示不同的資料。 輸入搜尋值和選取 Submit 時:

  • 搜尋字串變更。
  • searchString 參數不是 null。
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

PaginatedList.CreateAsync 方法會以支援分頁的集合類型,將學生查詢轉換成學生單一頁面。 該單一頁面的學生會傳遞至 Razor Page。

Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

PaginatedList.CreateAsync 中的兩個問號代表 null 聯合運算子。 Null 聯合運算子會針對可為 Null 的型別定義一個預設值。 運算式 (pageIndex ?? 1) 表示,如果 pageIndex 有一個值就將該值傳回。 如果 pageIndex 沒有值,則傳回 1。

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

執行應用程式並巡覽至學生頁面。

  • 若要確定分頁運作正常,請以不同排序次序按一下分頁連結。
  • 若要驗證分頁的排序和篩選能正確運作,請輸入搜尋字串並嘗試分頁。

students index page with paging links

若要更深入了解這個程式碼:

  • Students/Index.cshtml.cs,在 switch (sortOrder) 上設定中斷點。
  • NameSortDateSortCurrentSortModel.Student.PageIndex 新增監看式。
  • Students/Index.cshtml,在 @Html.DisplayNameFor(model => model.Student[0].LastName) 上設定中斷點。

逐步執行偵錯工具。

更新 About 頁面以顯示學生統計資料

在此步驟中,Pages/About.cshtml 會更新為顯示在每一個註冊日期中,共有多少學生註冊。 此更新會使用群組,並包含下列步驟:

  • About 頁面所使用的資料,建立檢視模型。
  • 更新 About 頁面以使用檢視模型。

建立檢視模型

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

更新 About 頁面模型

ASP.NET Core 2.2 中的 Web 範本不包括 [關於] 頁面。 若您使用 ASP.NET Core 2.2,請建立 [關於 Razor Page]。

以下列程式碼更新 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 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>

執行應用程式並巡覽至 About 頁面。 每個註冊日期的學生人數將會顯示在資料表中。

若您遭遇到無法解決的問題,請下載此階段的完整應用程式

About page

其他資源

在下一個教學課程中,應用程式將會使用移轉來更新資料模型。