チュートリアル: ASP.NET MVC アプリケーションに Entity Framework を使った並べ替え、フィルター処理、ページングを追加する

前のチュートリアルでは、Student エンティティの基本的な CRUD 操作のための一連の Web ページを実装しました。 このチュートリアルでは、Students の Index ページに並べ替え、フィルター処理、ページングの機能を追加します。 また、シンプルなグループ化ページも作成します。

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

Students_Index_page_with_paging

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

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

前提条件

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

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

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

    public ActionResult Index(string sortOrder)
    {
       ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
       ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
       var students = from s in db.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(students.ToList());
    }
    

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

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

ビューで適切なクエリ文字列値を使用して列見出しのハイパーリンクを構成できるように、2 つの ViewBag 変数が使用されます。

ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

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

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

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

並べ替え順序ごとに異なる LINQ ステートメントを記述する代わりに、LINQ ステートメントを動的に作成することもできます。 動的 LINQ について詳しくは、動的 LINQ に関するページを参照してください。

  1. Views\Student\Index.cshtml で、見出し行の <tr> 要素と <th> 要素を強調表示されたコードに置き換えます。

    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    <table class="table">
        <tr>
            <th>
                @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm })
            </th>
            <th>First Name
            </th>
            <th>
                @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm })
            </th>
            <th></th>
        </tr>
    
    @foreach (var item in Model) {
    

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

  2. ページを実行し、[姓] 列と [登録日] 列の見出しをクリックして、並べ替えが機能することを確認します。

    [姓] 見出しを クリックすると、学生は姓の降順で表示されます。

Students インデックス ページにフィルターを追加するには、テキスト ボックスと送信ボタンをビューに追加し、Index メソッドに対応する変更を行います。 テキスト ボックスを使って、姓と名のフィールドで検索する文字列を入力できます。

Index メソッドにフィルター機能を追加する

  • Controllers\StudentController.cs で、Index メソッドを次のコードで置き換えます (変更部分が強調表示されています)。

    public ViewResult Index(string sortOrder, string searchString)
    {
        ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
        var students = from s in db.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(students.ToList());
    }
    

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

Note

多くの場合、同じメソッドを Entity Framework エンティティ セットで呼び出すか、メモリ内コレクションの拡張メソッドとして呼び出すことができます。 結果は通常同じですが、場合によっては異なる場合があります。

たとえば、Contains メソッドの .NET Framework 実装では、空の文字列を渡すとすべての行が返されますが、SQL Server Compact 4.0 の Entity Framework プロバイダーは空の文字列に対して 0 行を返します。 したがって、この例のコード (Where ステートメントを if ステートメント内に配置) では、すべてのバージョンの SQL Server で確実に同じ結果が得られるようになっています。 また、Contains メソッドの .NET Framework 実装では、既定では大文字と小文字を区別しますが、Entity Framework SQL Server プロバイダーでは既定で大文字と小文字が区別されません。 したがって、ToUpper メソッドを呼び出してテストで明示的に大文字と小文字を区別しないようにすると、後でリポジトリを使用するようにコードを変更しても結果が変わらないようにできます。これにより、IQueryable オブジェクトではなく IEnumerable コレクションが返されます。 (IEnumerable コレクションに対して Contains メソッドを呼び出したときには、.NET Framework の実装を取得します。IQueryable オブジェクトに対して呼び出したときには、データベース プロバイダーの実装を取得します)。

null 値の処理も、データベース プロバイダーによって、または IQueryable オブジェクトを使う場合と IEnumerable コレクションを使う場合とで、異なることがあります。 たとえば、一部のシナリオでは、table.Column != 0 のような Where 条件では値に null を持つ列が返されないことがあります。 既定では、null 値間の等価性がデータベース内でもメモリ内のように動作するように、EF によって追加の SQL 演算子が生成されますが、EF6 で UseDatabaseNullSemantics フラグを設定する、または EF Core で UseRelationalNulls メソッドを呼び出すことで、この動作を構成できます。

Students インデックス ビューに検索ボックスを追加する

  1. Views\Student\Index.cshtml で、開き table タグの直前に次の強調表示されたコードを追加して、キャプション、テキスト ボックス、および [Search] ボタンを作成します。

    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    
    @using (Html.BeginForm())
    {
        <p>
            Find by name: @Html.TextBox("SearchString")  
            <input type="submit" value="Search" /></p>
    }
    
    <table>
        <tr>
    
  2. ページを実行し、検索文字列を入力し、[検索] をクリックしてフィルター処理が機能していることを確認します。

    URL に検索文字列の "an" が含まれていないことに注意してください。つまり、このページをブックマークしても、ブックマークを使ってフィルター処理されたリストを取得することはできません。 これは、列の並べ替えリンクにも該当します。リスト全体が並べ替えられるためです。 チュートリアルの後半では、フィルター条件にクエリ文字列を使用するように [検索] ボタンを変更します。

ページングの追加

Students インデックス ページにページングを追加するには、まず PagedList.Mvc NuGet パッケージをインストールします。 その次に、Index メソッドに追加の変更を加え、ページング リンクを Index ビューに追加します。 PagedList.Mvc は、ASP.NET MVC の多くの優れたページングおよび並べ替えパッケージの 1 つであり、ここでの使用は、あくまで例としてであり、他のオプションより推奨されるということではありません。

PagedList.MVC NuGet パッケージをインストールする

NuGet PagedList.Mvc パッケージは、PagedList パッケージを依存関係として自動的にインストールします。 PagedList パッケージは、IQueryable および IEnumerable コレクションに対する PagedList コレクションの種類と拡張メソッドをインストールします。 拡張メソッドは、IQueryable または IEnumerable から PagedListコレクション内に 1 ページのデータを作成し、 PagedList コレクションにはページングを容易にする複数のプロパティとメソッドが用意されています。 PagedList.Mvc パッケージは、ページング ボタンを表示するページング ヘルパーをインストールします。

  1. [ツール] メニューで、[NuGet パッケージ マネージャー]、[パッケージ マネージャー コンソール] の順に選択します。

  2. [パッケージ マネージャー コンソール] ウィンドウで、[パッケージ ソース] が nuget.org であり、[既定のプロジェクト] が ContosoUniversity であることを確認してから、次のコマンドを入力します。

    Install-Package PagedList.Mvc
    
  3. プロジェクトをビルドします。

Note

PageList パッケージはもう保守されていません。 そのため、最新のプロジェクトでは X.PagedList パッケージを使うことをお勧めします。 主な違いは、X.PagedList は移植可能なアセンブリであるという点です。 つまり、このパッケージはクロスプラットフォームであり、Web プロジェクトでも、他の .NET プロジェクトでも使用できます。 新しいパッケージはバージョン 8.4 から .NET 6 に移植されているため、互換性の問題は生じないはずです。

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

  1. Controllers\StudentController.csで、PagedList 名前空間の using ステートメントを追加します。

    using PagedList;
    
  2. Index メソッドを次のコードで置き換えます。

    public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
    {
       ViewBag.CurrentSort = sortOrder;
       ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
       ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
    
       if (searchString != null)
       {
          page = 1;
       }
       else
       {
          searchString = currentFilter;
       }
    
       ViewBag.CurrentFilter = searchString;
    
       var students = from s in db.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:  // Name ascending 
             students = students.OrderBy(s => s.LastName);
             break;
       }
    
       int pageSize = 3;
       int pageNumber = (page ?? 1);
       return View(students.ToPagedList(pageNumber, pageSize));
    }
    

    このコードでは、page パラメーター、現在の並べ替え順序パラメーター、現在のフィルター パラメーターをメソッド シグネチャに追加しています。

    public ActionResult Index(string sortOrder, string currentFilter, string searchString, int? page)
    

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

    ViewBag プロパティは、現在の並べ替え順序でビューを提供します。ページング時に並べ替え順序を維持するには、これをページング リンクに含める必要があるためです。

    ViewBag.CurrentSort = sortOrder;
    

    もう 1 つのプロパティ ViewBag.CurrentFilter は、ビューに現在のフィルター文字列を提供します。 ページング中にフィルターの設定を維持するために、ページングのリンクにこの値を含める必要があり、ページが再表示されるときに、この値をテキスト ボックスに復元する必要があります。 ページングの中に検索文字列を変更した場合は、新しいフィルターのために別のデータが表示されるため、ページを 1 にリセットする必要があります。 テキスト ボックスに値を入力して、[Submit] ボタンを押したときに、検索文字列が変更されます。 その場合は、searchString パラメーターは null ではありません。

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

    メソッドの最後に、学生 IQueryable オブジェクトの ToPagedList 拡張メソッドは、学生クエリを、ページングをサポートするコレクション型の学生の単一ページに変換します。 そして、この学生の単一ページがビューに渡されます。

    int pageSize = 3;
    int pageNumber = (page ?? 1);
    return View(students.ToPagedList(pageNumber, pageSize));
    

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

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

    @model PagedList.IPagedList<ContosoUniversity.Models.Student>
    @using PagedList.Mvc;
    <link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />
    
    @{
        ViewBag.Title = "Students";
    }
    
    <h2>Students</h2>
    
    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    @using (Html.BeginForm("Index", "Student", FormMethod.Get))
    {
        <p>
            Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
            <input type="submit" value="Search" />
        </p>
    }
    <table class="table">
        <tr>
            <th>
                @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
            </th>
            <th>
                First Name
            </th>
            <th>
                @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter=ViewBag.CurrentFilter })
            </th>
            <th></th>
        </tr>
    
    @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>
                @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
                @Html.ActionLink("Details", "Details", new { id=item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id=item.ID })
            </td>
        </tr>
    }
    
    </table>
    <br />
    Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
    
    @Html.PagedListPager(Model, page => Url.Action("Index", 
        new { page, sortOrder = ViewBag.CurrentSort, currentFilter = ViewBag.CurrentFilter }))
    

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

    PagedList.Mvc で using ステートメントを使用することで、ページング ボタンの MVC ヘルパーにアクセスできます。

    このコードでは、BeginForm のオーバーロードを使用することで、FormMethod.Get を指定できます。

    @using (Html.BeginForm("Index", "Student", FormMethod.Get))
    {
        <p>
            Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)  
            <input type="submit" value="Search" />
        </p>
    }
    

    既定の BeginForm は、POST を使用してフォーム データを送信します。これは、パラメーターが、URL 内のクエリ文字列として渡されるのではなく、HTTP メッセージの本文で渡されることを意味します。 HTTP GET を指定すると、フォーム データがクエリ文字列として URL で渡され、ユーザーが URL をブックマークできるようになります。 HTTP GET の使用に関する W3C ガイドラインでは、アクションの結果更新が発生しない場合は GET を使うことが推奨されています。

    テキスト ボックスは現在の検索文字列で初期化されるため、新しいページをクリックすると、現在の検索文字列が表示されます。

    Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
    

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

    @Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
    

    現在のページとページの合計数が表示されます。

    Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
    

    表示するページがない場合は、"Page 0 of 0" が表示されます。 (その場合、Model.PageNumber は 1 で、Model.PageCount は 0 であるため、ページ番号はページ数より大きくなります。)

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

    @Html.PagedListPager( Model, page => Url.Action("Index", new { page }) )
    

    PagedListPager ヘルパーには、URL やスタイル設定など、カスタマイズできるさまざまなオプションが用意されています。 詳細については、GitHub サイト TroyGoode/PagedList を参照してください。

  2. このページを実行します。

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

About ページを作成する

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

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

ビュー モデルの作成

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

using System;
using System.ComponentModel.DataAnnotations;

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

        public int StudentCount { get; set; }
    }
}

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

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

    using ContosoUniversity.DAL;
    using ContosoUniversity.ViewModels;
    
  2. クラスの開き中かっこの直後に、データベース コンテキストのクラス変数を追加します。

    public class HomeController : Controller
    {
        private SchoolContext db = new SchoolContext();
    
  3. About メソッドを次のコードで置き換えます。

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

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

  4. Dispose メソッドを追加します。

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
    

About ビューを変更する

  1. Views\Home\About.cshtml ファイルのコードを次のコードに置き換えます。

    @model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup>
               
    @{
        ViewBag.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>
    
  2. アプリを実行し、[バージョン情報] リンクをクリックします。

    登録日ごとの学生の数が、テーブルに表示されます。

    About_page

コードを取得する

完成したプロジェクトをダウンロードする

その他のリソース

他の Entity Framework リソースへのリンクは、「ASP.NET データ アクセス - 推奨リソース」にあります。

次のステップ

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

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

次の記事に進み、接続の回復性とコマンド インターセプトを使う方法について学習しましょう。