チュートリアル: 並べ替え、フィルター処理、ページングを追加する - ASP.NET MVC と EF Core

前のチュートリアルでは、Student エンティティの基本的な CRUD 操作用の Web ページのセットを実装しました。 このチュートリアルでは、Students インデックス ページに並べ替え、フィルター、およびページング機能を追加します。 単純なグループ化を実行するページも作成します。

次の図は、作業が終了したときにページがどのように表示されるかを示しています。 列見出しとは、ユーザーがクリックしてその列で並べ替えを行うことができるリンクです。 列見出しを繰り返しクリックすると、昇順と降順の並べ替え順序が切り替えられます。

Students index page

このチュートリアルでは、次の作業を行いました。

  • 列の並べ替えリンクを追加する
  • [検索] ボックスを追加する
  • Students/Index にページングを追加する
  • Index メソッドにページングを追加する
  • ページング リンクを追加する
  • About ページを作成する

必須コンポーネント

Students ンデックス ページに並べ替えを追加するには、IndexStudents コントローラーのメソッドを変更し、Students インデックス ビューにコードを追加します。

Index メソッドに並べ替え機能を追加する

StudentsController.cs で、Index メソッドを次のコードに置き換えます。

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

このコードは、URL 内の文字列から sortOrder パラメーターを受け取ります。 クエリ文字列の値は、ASP.NET Core MVC によってパラメーターとしてアクション メソッドに提供されます。 パラメータは、"Name" または "Date" という文字列で、その後に必要に応じてアンダースコアと降順を指定する文字列 "desc" が続きます。 既定の並べ替え順序は昇順です。

インデックス ページが初めて要求されたときには、クエリ文字列はありません。 受講者は、姓の昇順で表示されます。これは、switch ステートメントでフォールスルー ケースによって確立される既定値です。 ユーザーが列見出しハイパーリンクをクリックすると、適切な sortOrder 値がクエリ文字列で提供されます。

2 つの ViewData 要素 (NameSortParm と DateSortParm) をビューで使用して、適切なクエリ文字列値を持つ列見出しのハイパーリンクを構成します。

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

これらは、三項ステートメントです。 最初のステートメントは、sortOrder パラメーターが null または空の場合に、NameSortParm を "name_desc" に設定し、それ以外の場合は任意の空の文字列に設定する必要があることを指定します。 これらの 2 つのステートメントを使用して、次のようにビューで列見出しのハイパーリンクの設定することができます。

既定の並べ替え順 姓のハイパーリンク 日付のハイパーリンク
姓の昇順 descending ascending
姓の降順 ascending ascending
日付の昇順 ascending descending
日付の降順 ascending ascending

このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。 このコードは、switch ステートメントの前に IQueryable 変数を作成し、switch ステートメントでそれを変更して、switch ステートメントの後に ToListAsync を呼び出します。 IQueryable 変数を作成および変更するときに、データベースに送信されるクエリはありません。 クエリは、ToListAsync などのメソッドを呼び出すことによって IQueryable オブジェクトをコレクションに変換するまで実行されません。 そのため、このコードの結果として、return View ステートメントはで実行されない 1 つのクエリになります。

このコードは、多数の列によって冗長になる可能性があります。 このシリーズの最後のチュートリアルでは、文字列変数で OrderBy 列の名前を渡すことができるコードの記述方法を説明しています。

Views/Students/Index.cshtml のコードを次のコードに置き換え、列見出しのハイパーリンクを追加します。 変更された行は強調表示されています。

@model IEnumerable<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

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

このコードは、ViewData プロパティ内の情報を使用して、適切なクエリ文字列使用含むハイパーリンクを設定します。

アプリを実行し、 [Students] タブを選択して、 [Last Name][Enrollment Date] 列見出しをクリックし、並べ替えが機能することを確認します。

Students index page in name order

Students インデックス ページにフィルターを追加するには、テキスト ボックスと送信ボタンをビューに追加し、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());
}

searchString パラメーターを Index メソッドに追加しました。 インデックス ビューに追加するテキスト ボックスから検索する文字列値を受け取ります。 さらに LINQ ステートメントに where 句を追加し、姓または名に検索文字列を含む受講者のみを選択します。 where 句を追加するステートメントは、検索する値がある場合にのみ、実行されます。

Note

ここで、IQueryable オブジェクトに対して Where メソッドを呼び出し、フィルターがサーバーで処理されます。 一部のシナリオでは、Where メソッドをメモリ内コレクションの拡張メソッドとして呼び出す場合があります (たとえば、EF DbSet の代わりに IEnumerable コレクションを返すリポジトリ メソッドを参照するように参照を _context.Students に変更する場合)。結果は、通常同じになりますが、場合によっては異なる場合があります。

たとえば、.NET Framework の Contains メソッドの実装は、既定では大文字小文字を区別する比較を実行しますが、SQL Server では、これは SQL Server インスタンスの照合順序の設定によって決まります。 その設定は、既定では大文字小文字を区別しません。 Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper()) のように、ToUpper メソッドを呼び出して明示的に大文字小文字を区別しないテストを作成することもできます。 これにより、IQueryable オブジェクトの代わりに IEnumerable コレクションを返すリポジトリを使用するように後でコードを変更した場合でも確実に同じ結果になるようにすることができます。 (IEnumerable コレクションに対して Contains メソッドを呼び出したときには、.NET Framework の実装を取得します。IQueryable オブジェクトに対して呼び出したときには、データベース プロバイダーの実装を取得します)。ただし、このソリューションではパフォーマンスが低下します。 ToUpper コードは、TSQL SELECT ステートメントの WHERE 句に関数を格納します。 これにより、オプティマイザーはインデックスを使用できなくなります。 ほとんどの場合、SQL は大文字小文字を区別しないようにインストールされているため、大文字小文字を区別するデータストアに移行するまで ToUpper コードを避けることをお勧めします。

Students インデックス ビューに [Search] ボックスを追加する

Views/Student/Index.cshtml で、キャプション、テキスト ボックス、 [Search] ボタンを作成するために、オープニング テーブル タグの直前に強調表示されたコードを追加します。

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

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

<table class="table">

このコードは、<form>タグ ヘルパーを使用して、検索テキスト ボックスとボタンを追加します。 既定では、<form> タグ ヘルパーは、POST を使用してフォーム データを送信します。これは、パラメーターが、クエリ文字列として URL で渡されるのではなく、HTTP メッセージの本文で渡されることを意味します。 HTTP GET を指定すると、フォーム データがクエリ文字列として URL で渡され、ユーザーが URL をブックマークできるようになります。 アクションの結果として更新されない場合、W3C のガイドラインでは、Get の使用が推奨されます。

アプリを実行し、 [Students] タブを選択して、検索文字列を入力し、[Search] をクリックして、フィルターが機能していることを確認します。

Students index page with filtering

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

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

このページをブックマークに設定した場合、ブックマークを使用するときに、フィルター処理された一覧が表示されます。 method="get"form タグに追加すると、クエリ文字列が生成されます。

この段階で、列見出しのソートのリンクをクリックすると、 [Search] ボックスに入力したフィルター値が失われます。 次のセクションでこれを修正します。

Students/Index にページングを追加する

Students インデックス ページにページングを追加するには、常にテーブルをすべての行を取得する代わりに、Skip および Take ステートメントを使用してサーバー上でデータをフィルター処理する PaginatedList クラスを作成します。 その後で、Index メソッドに変更を加え、ページング ボタンを Index ビューに追加します。 ページング ボタンを次の図に示します。

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>オブジェクトを作成します。

Index メソッドにページングを追加する

StudentsController.cs で、Index メソッドを次のコードに置き換えます。

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

    if (searchString != null)
    {
        pageNumber = 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(), pageNumber ?? 1, pageSize));
}

このコードは、メソッド シグネチャにページ番号パラメーター、現在の並べ替え順序パラメーター、および現在のフィルター パラメーターを追加します。

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

最初にページが表示されるとき、またはユーザーがページングや並べ替えのリンクをクリックしていない場合、すべてのパラメーターは null になります。 ページングのリンクをクリックすると、ページ変数に表示するページ番号が含まれます。

CurrentSort という名前の ViewData 要素が、現在の並べ替え順序をビューに提供します。ページング中に同じ並べ替え順序を維持するために、ページングのリンクにこれを含める必要があります。

CurrentFilter という名前の ViewData 要素が現在のフィルター文字列をビューに提供します。 ページング中にフィルターの設定を維持するために、ページングのリンクにこの値を含める必要があり、ページが再表示されるときに、この値をテキスト ボックスに復元する必要があります。

ページングの中に検索文字列を変更した場合は、新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 テキスト ボックスに値を入力して、[Submit] ボタンを押したときに、検索文字列が変更されます。 その場合は、searchString パラメーターは null ではありません。

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

Index メソッドの最後に、PaginatedList.CreateAsync メソッドが、ページングをサポートするコレクション型の受講者の 1 つのページに受講者クエリを変換します。 その 1 つの受講者のページがビューに渡されます。

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

PaginatedList.CreateAsync メソッドは、ページ番号を受け取ります。 2 つの疑問符は、null 合体演算子を表します。 null 合体演算子は null 許容型の既定値を定義します。式 (pageNumber ?? 1) は、値がある場合は pageNumber の値を返し、pageNumber が null の場合は 1 を返すことを意味します。

Views/Students/Index.cshtml で、既存のコードを次のコードに置き換えます。 変更が強調表示されます。

@model PaginatedList<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

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

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

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

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

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(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-pageNumber="@(Model.PageIndex + 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @nextDisabled">
    Next
</a>

ページの上部にある @model ステートメントは、ビューが List<T> オブジェクトの代わりに PaginatedList<T> オブジェクトを取得するようになったことを指定します。

列ヘッダーへのリンクは、フィルターの結果内でユーザーが並べ替えられるように、クエリ文字列を使用してコントローラーに現在の検索文字列を渡します。

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

タグ ヘルパーによってページング ボタンが表示されます。

<a asp-action="Index"
   asp-route-sortOrder="@ViewData["CurrentSort"]"
   asp-route-pageNumber="@(Model.PageIndex - 1)"
   asp-route-currentFilter="@ViewData["CurrentFilter"]"
   class="btn btn-default @prevDisabled">
   Previous
</a>

アプリを実行して [Students] ページに移動します。

Students index page with paging links

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

About ページを作成する

Contoso 大学の Web サイトの [About] ページに、登録日付ごとに登録した受講者の数が表示されます。 これには、グループ化とグループに関する簡単な計算が必要です。 これを実行するためには、次の手順を実行します。

  • ビューに渡す必要があるデータのビュー モデル クラスを作成します。
  • Home コントローラーで About メソッドを作成します。
  • About ビューを作成します。

ビュー モデルを作成する

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

新しいフォルダー内に、EnrollmentDateGroup.cs という名前のクラス ファイルを追加し、テンプレート コードを次のコードに置き換えます。

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Home コントローラーを変更する

HomeController.cs で、ファイルの先頭に次のステートメントを追加します。

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

クラスの左中かっこの直後に、データベース コンテキストのクラス変数を追加し、ASP.NET Core DI からコンテキストのインスタンスを取得します。

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly SchoolContext _context;

    public HomeController(ILogger<HomeController> logger, SchoolContext context)
    {
        _logger = logger;
        _context = context;
    }

次のコードを含む About メソッドを追加します。

public async Task<ActionResult> About()
{
    IQueryable<EnrollmentDateGroup> data = 
        from student in _context.Students
        group student by student.EnrollmentDate into dateGroup
        select new EnrollmentDateGroup()
        {
            EnrollmentDate = dateGroup.Key,
            StudentCount = dateGroup.Count()
        };
    return View(await data.AsNoTracking().ToListAsync());
}

LINQ ステートメントは、登録日で受講者エンティティをグループ化し、各グループ内のエンティティの数を計算して、結果を EnrollmentDateGroup ビュー モデル オブジェクトのコレクションに格納します。

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>

アプリを実行して [About] ページに移動します。 登録の日付ごとの学生の数が、テーブルに表示されます。

コードを取得する

完成したアプリケーションをダウンロードまたは表示する。

次の手順

このチュートリアルでは、次の作業を行いました。

  • 列の並べ替えリンクを追加した
  • [検索] ボックスを追加した
  • Students/Index にページングを追加した
  • Index メソッドにページングを追加した
  • ページング リンクを追加した
  • About ページを作成した

移行を使ってデータ モデルの変更を処理する方法について学習するには、次のチュートリアルに進んでください。