パート 3、ASP.NET Core の Razor ページと EF Core - 並べ替え、フィルター、ページング

作成者: 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 パラメーターは Name または Date のいずれかとなります。 sortOrder パラメーターの後には、必要に応じて、降順を指定する _desc が置かれます。 既定の並べ替え順序は昇順です。

インデックス ページが、Students リンクから要求された場合、クエリ文字列はありません。 学生は、姓の昇順で表示されます。 switch ステートメントでは、姓の昇順が default です。 ユーザーが列見出しリンクをクリックすると、適切な sortOrder 値がクエリ文字列値で提供されます。

NameSort および DateSort は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。

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

このコードによって、C# の条件演算子 ?: が使用されています。 ?: 演算子は三項演算子であり、3 つのオペランドを取ります。 最初の行により、sortOrder が null または空の場合に、NameSortname_desc に設定することが指定されます。 sortOrder が null でも空でも "ない" 場合、NameSort は空の文字列に設定されます。

これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。

既定の並べ替え順 姓のハイパーリンク 日付のハイパーリンク
姓の昇順 descending 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 コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。

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 列見出しにハイパーリンクを追加します。
  • この情報を NameSort および DateSort で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。
  • ページ見出しを Index から Students に変更します。
  • Model.StudentModel.Students に変更します。

並べ替えが動作することを確認するには

  • アプリを実行し、 [Students] タブを選択します。
  • 列見出しをクリックします。

フィルターの追加

Students インデックス ページにフィルターを追加するには

  • テキスト ボックスと [送信] ボタンが、Razor ページに追加されます。 テキスト ボックスは、名と姓で検索文字列を指定します。
  • テキスト ボックスの値を使用するようにページ モデルが更新されます。

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 プロパティのパラメーター値を保存します。 次のセクションで追加されるテキスト ボックスから検索する文字列値を受け取ります。
  • LINQ ステートメントに Where 句を追加します。 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())`

上記のコードでは、Where メソッドが IEnumerable で呼び出された場合でも、SQLite で実行された場合でも、フィルターの大文字と小文字が確実に区別されないようにします。

ContainsIEnumerable コレクションで呼び出されたときには、.NET Core の実装が使用されます。 ContainsIQueryable オブジェクトで呼び出されたときには、データベースの実装が使用されます。

通常、パフォーマンス上の理由から、IQueryable での Contains の呼び出しをお勧めします。 IQueryable では、データベース サーバーによってフィルター処理が行われます。 最初に IEnumerable を作成する場合は、すべての行がデータベース サーバーから返される必要があります。

ToUpper を呼び出すとパフォーマンスが低下します。 ToUpper コードは、TSQL SELECT ステートメントの WHERE 句に関数を追加します。 関数が追加されると、オプティマイザーがインデックスを使用できなくなります。 大文字小文字を区別しないように SQL がインストールされている場合、不要な場合は ToUpper を呼び出さないようにすることをお勧めします。

詳細については、「Sqlite プロバイダーで大文字と小文字を区別しないクエリを使用する方法」を参照してください。

Razor ページを更新する

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 を使用すると、URL ではなく HTTP メッセージの本文でパラメーターが渡されます。 HTTP GET を使用すると、フォームのデータはクエリ文字列として URL で渡されます。 クエリ文字列を使用してデータを渡すことにより、ユーザーが URL にブックマークを設定できます。 アクションの結果として更新されない場合、W3C のガイドラインでは、Get の使用が推奨されています。

アプリをテストします。

  • [Students] タブを選択し、検索文字列を入力します。 SQLite を使用している場合、前に示したオプションの ToUpper コードを実装した場合にのみ、フィルターの大文字と小文字が区別されません。

  • [Search] を選択します。

URL に検索文字列が含まれることに注意してください。 次に例を示します。

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

ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString クエリ文字列が含まれます。 form タグ内に method="get" があると、クエリ文字列が生成されます。

現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。 次のセクションでは、失われたフィルター値は修正されます。

ページングの追加

このセクションでは、ページングをサポートする 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 メソッドは、ページ サイズとページ番号を受け取り、適切な Skip および Take ステートメントを IQueryable に適用します。 IQueryableToListAsync が呼び出されると、要求されたページのみを含むリストを返します。 プロパティ HasPreviousPage および HasNextPage を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。

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> に変更します。
  • ページ インデックス、現在の sortOrdercurrentFilterOnGetAsync メソッド シグネチャに追加します。
  • 並べ替え順序を CurrentSort プロパティに保存します。
  • 新しい検索文字列がある場合に、ページ インデックスを 1 にリセットします。
  • PaginatedList クラスを使用して、Student エンティティを取得します。
  • pageSize構成から 3 に設定します。構成に失敗した場合は 4 を設定します。

OnGetAsync で受け取るパラメーターはすべて、次の場合に null になります。

  • Students リンクからページが呼び出されます。
  • ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。

ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。

CurrentSort プロパティでは、現在の並べ替え順序を含む Razor ページを提供します。 ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。

CurrentFilter プロパティでは、現在のフィルター文字列を含む Razor ページを提供します。 CurrentFilter 値:

  • ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。
  • ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。

ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 検索値が入力され、 [Submit] が選択された場合:

  • 検索文字列が変更されます。
  • searchString パラメーターは null ではありません。

PaginatedList.CreateAsync メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。 その 1 つの学生のページが Razor ページに渡されます。

PaginatedList.CreateAsync 呼び出しの pageIndex の後の 2 つの疑問符は、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] ページに移動します。

  • 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。
  • 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。

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 ページを作成する

次のコードを使用して、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 ステートメントでは姓の昇順が既定値 (フォールスルー ケース) です。 ユーザーが列見出しリンクをクリックすると、適切な sortOrder 値がクエリ文字列値で提供されます。

NameSort および DateSort は、Razor ページで、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成するために使用されます。

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 は三項演算子とも呼ばれます。

これらの 2 つのステートメントを使用して、次のようにページで列見出しのハイパーリンクを設定することができます。

既定の並べ替え順 姓のハイパーリンク 日付のハイパーリンク
姓の昇順 descending 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 コードの結果として、次のステートメントまで実行されない 1 つのクエリになります。

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 列見出しにハイパーリンクを追加します。
  • この情報を NameSort および DateSort で使用して、現在の並べ替えの値を含むハイパーリンクを設定します。

並べ替えが動作することを確認するには

  • アプリを実行し、 [Students] タブを選択します。
  • [Last Name] をクリックします。
  • [Enrollment Date] をクリックします。

コードの理解を深めるために、次の手順を実行します。

  • Students/Index.cshtml.cs で、switch (sortOrder) にブレークポイントを設定します。
  • NameSortDateSort のウォッチを追加します。
  • Students/Index.cshtml で、@Html.DisplayNameFor(model => model.Student[0].LastName) にブレークポイントを設定します。

デバッガーの手順を実行します。

Students インデックス ページに [検索] ボックスを追加する

Students インデックス ページにフィルターを追加するには

  • テキスト ボックスと [送信] ボタンが、Razor ページに追加されます。 テキスト ボックスは、名と姓で検索文字列を指定します。
  • テキスト ボックスの値を使用するようにページ モデルが更新されます。

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 メソッドに追加します。 次のセクションで追加されるテキスト ボックスから検索する文字列値を受け取ります。
  • LINQ ステートメントに Where 句を追加します。 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 を使用するように変更された場合、結果で大文字小文字が区別されないようにします。 ContainsIEnumerable コレクションで呼び出されたときには、.NET Core の実装が使用されます。 ContainsIQueryable オブジェクトで呼び出されたときには、データベースの実装が使用されます。 リポジトリから IEnumerable を返すと、パフォーマンスが大幅に低下する可能性があります。

  1. DB サーバーからすべての行が返されます。
  2. アプリケーションで返されたすべての行にフィルターが適用されます。

ToUpper を呼び出すとパフォーマンスが低下します。 ToUpper コードは、TSQL SELECT ステートメントの WHERE 句に関数を追加します。 関数が追加されると、オプティマイザーがインデックスを使用できなくなります。 大文字小文字を区別しないように SQL がインストールされている場合、不要な場合は ToUpper を呼び出さないようにすることをお勧めします。

Students インデックス ページに [検索] ボックスを追加する

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 を使用すると、URL ではなく HTTP メッセージの本文でパラメーターが渡されます。 HTTP GET を使用すると、フォームのデータはクエリ文字列として URL で渡されます。 クエリ文字列を使用してデータを渡すことにより、ユーザーが URL にブックマークを設定できます。 アクションの結果として更新されない場合、W3C のガイドラインでは、Get の使用が推奨されています。

アプリをテストします。

  • [Students] タブを選択し、検索文字列を入力します。
  • [Search] を選択します。

URL に検索文字列が含まれることに注意してください。

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

ブックマークがブックマークに設定されている場合、ブックマークにページの URL と SearchString クエリ文字列が含まれます。 form タグ内に method="get" があると、クエリ文字列が生成されます。

現時点では、列見出しの並べ替えリンクを選択すると、フィルター値が [Search] ボックスから失われます。 次のセクションでは、失われたフィルター値は修正されます。

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 メソッドは、ページ サイズとページ番号を受け取り、適切な Skip および Take ステートメントを IQueryable に適用します。 IQueryableToListAsync が呼び出されると、要求されたページのみを含むリストを返します。 プロパティ HasPreviousPage および HasNextPage を使用して、 [Previous] および [Next] ページング ボタンを有効または無効にします。

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

上記のコードは、ページ インデックス、現在の sortOrder、および currentFilter をメソッド シグネチャに追加します。

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

すべてのパラメーターは、次のような場合に null になります。

  • Students リンクからページが呼び出されます。
  • ユーザーは、ページングまたは並べ替えのリンクをクリックしていません。

ページングのリンクをクリックすると、ページ インデックス変数に表示するページ番号が含まれます。

CurrentSort は、現在の並べ替え順序を含む Razor ページを提供します。 ページングの中に並べ替え順序を保持するために、ページングリンクに、現在の並べ替え順序を含まれている必要があります。

CurrentFilter は、現在のフィルター文字列を含む Razor ページを提供します。 CurrentFilter 値:

  • ページングの中に、フィルターの設定を維持するために、ページング リンクに含まれている必要があります。
  • ページがリダイレクトされるときに、テキスト ボックスに復元される必要があります。

ページングの中に検索文字列を変更する場合は、ページが 1 にリセットされます。 新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 検索値が入力され、 [Submit] が選択された場合:

  • 検索文字列が変更されます。
  • searchString パラメーターは null ではありません。
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

PaginatedList.CreateAsync メソッドが、ページングをサポートするコレクション型の学生の 1 つのページに学生クエリを変換します。 その 1 つの学生のページが Razor ページに渡されます。

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

PaginatedList.CreateAsync の 2 つの疑問符は、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] ページに移動します。

  • 異なる並べ替え順でページングのリンクをクリックし、ページングが機能することを確認します。
  • 検索文字列を入力して、ページングを試し、並べ替えとフィルター処理を使用してもページングが正しく機能することを確認します。

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 ページを更新します。

ビュー モデルを作成する

SchoolViewModels フォルダーを Models フォルダー内に作成します。

次のコードを使用して、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 テンプレートには、About ページが含まれていません。 ASP.NET Core 2.2 を使っている場合は、About Razor ページを作成します。

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 ビュー モデル オブジェクトのコレクションに格納します。

About Razor ページを変更する

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

その他のリソース

次のチュートリアルでは、アプリは移行を使用してデータ モデルを更新します。