前のチュートリアルでは、Student エンティティの基本的な CRUD 操作用の Web ページのセットを実装しました。 このチュートリアルでは、並べ替え、フィルター処理、ページングの機能を [Students Index] ページに追加します。 また、単純なグループ化を行うページも作成します。
この演習では、並べ替え、フィルター処理、ページングの機能を [Students Index] ページに追加します。 また、単純なグループ化を行うページも作成します。 次の図は、完了時のページの外観を示しています。 列見出しは、ユーザーがその列で並べ替えるために選択できるリンクです。 列見出しを繰り返し選択すると、並べ替え順序が昇順と降順の間で切り替わります。
このチュートリアルでは、次の作業を行いました。
- 列の並べ替えをサポートするためのリンクを追加する
- 検索をサポートする検索ボックスを追加する
- Students Index にページングを追加する
- ページング アクションをサポートするためのリンクを追加する
- サイトの概要ページを作成する
必須コンポーネント
- 前のチュートリアル「ASP.NET MVC を使用して基本的な CRUD 機能を実装するEF Core」を完了します。
列の並べ替えリンクを追加する
[Students Index]\(学生インデックス\) ページに並べ替えを追加するには、Students コントローラーのIndexメソッドを変更し、Student Index ビューにコードを追加します。
Index メソッドに並べ替え機能を追加する
StudentsController.cs ファイルで、Index メソッドを次のコードに置き換えます。
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
このコードは、URL 内の文字列から sortOrder パラメーターを受け取ります。 クエリ文字列の値は、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 つのステートメントを使用して、次のようにビューで列見出しのハイパーリンクの設定することができます。
| 既定の並べ替え順 | 姓ハイパーリンク | 日付ハイパーリンク |
|---|---|---|
| 姓の昇順 | [降順] | [昇順] |
| 姓の降順 | [昇順] | 昇順 |
| 日付の昇順 | [昇順] | [降順] |
| 日付の降順 | [昇順] | [昇順] |
このメソッドは、並べ替える列を指定するのに LINQ to Entities を使用します。 このコードでは、IQueryable ステートメントの前にswitch変数を作成し、switch ステートメントで変数を変更し、ToListAsync ステートメントの後に switch メソッドを呼び出します。
IQueryable 変数を作成および変更するときに、データベースに送信されるクエリはありません。 クエリは、IQueryable などのメソッドを呼び出すことによって ToListAsync オブジェクトをコレクションに変換するまで実行されません。 したがって、このコードは、 return View ステートメントまで実行されない 1 つのクエリになります。
このコードは、多数の列を操作する場合に冗長になることがあります。 このシリーズの最後のチュートリアル では、「動的 LINQ を使用してコードを簡略化する > 高度なシナリオについて説明します。このチュートリアルでは、文字列変数に OrderBy 列の名前を渡すコードを記述する方法を示します。
列見出しハイパーリンクを Student インデックス ビューに追加する
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 Index]\(学生インデックス\) ページにフィルター処理を追加するには、テキスト ボックスと送信ボタンをビューに追加し、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 パラメーターを追加しました。 検索文字列の値は、インデックス ビューに追加したテキスト ボックスから受け取ります。 また、 where 句を LINQ ステートメントに追加しました。この句では、名または姓が検索文字列と一致する学生のみを選択します。
where句を追加するステートメントは、検索可能な値がある場合にのみ実行されます。
Where メソッドの使用方法を決定する
このシナリオでは、Where オブジェクトで IQueryable メソッドを呼び出し、サーバーでフィルターが処理されます。 一部のシナリオでは、 Where メソッドをメモリ内コレクションの拡張メソッドとして呼び出す場合があります。 参照を _context.Students に変更するとします。 Entity Framework DbSet メソッドを呼び出す代わりに、 IEnumerable コレクションを返すリポジトリ メソッドを参照します。 通常、結果は同じですが、場合によっては異なる場合があります。
たとえば、 Contains メソッドの .NET Framework 実装では、既定で大文字と小文字が区別される比較が実行されます。 SQL Server では、決定は SQL Server インスタンスの照合順序設定によって行われます。 デフォルトでは、設定は大文字と小文字を区別しません。
ToUpper メソッドを呼び出して、テストで明示的に大文字と小文字を区別しないようにすることができます:Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())。 このオプションを使用すると、後でコードを変更して、IEnumerable オブジェクトではなくIQueryable コレクションを返すリポジトリを使用するように変更した場合でも、結果は同じままになります。 (Contains コレクションで IEnumerable メソッドを呼び出すと、.NET Framework の実装が取得されます。IQueryable オブジェクトで呼び出すと、データベース プロバイダーの実装が取得されます)。
ただし、このソリューションにはパフォーマンスに影響が生じる可能性があります。
ToUpper コードは、WHERE ステートメントの TSQL SELECT 句に関数を配置します。 この動作により、オプティマイザーはインデックスを使用できなくなります。 SQL は主に大文字と小文字を区別しない状態でインストールされるため、大文字と小文字を区別しないデータ ストアに移行するまでは、 ToUpper コードを使用しないことをお勧めします。
Student Index ビューに検索ボックスを追加する
Views/Student/Index.cshtml ファイルで、キャプション、テキスト ボックス、<table>ボタンを作成するために、開始テーブル タグの直前に強調表示されたコードを追加します。
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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 を使用することをお勧めします。
アプリを実行し、[ 学生 ] タブを選択して検索文字列を入力し、[ 検索 ] を選択してフィルター処理が機能していることを確認します。
URL に検索文字列が含まれることに注意してください。
http://localhost:5813/Students?SearchString=an
このページをブックマークすると、ブックマークを使用するときにフィルター処理された一覧が表示されます。
method="get"句を form タグに追加すると、クエリ文字列が作成されます。
開発中のこの段階で列見出しの並べ替えリンクを選択すると、[ 検索 ] ボックスに入力したフィルター値が失われます。 この状況に対処する方法については、次のセクションで説明します。
Students Index にページングを追加する
[Students Index]\(学生インデックス\) ページにページングを追加するには、常にすべてのテーブル行を取得するのではなく、PaginatedListステートメントとSkip ステートメントを使用してサーバー上のデータをフィルター処理するTake クラスを作成します。 次に、 Index メソッドをさらに変更し、ページング ボタンを Index ビューに追加します。 次の図は、ページング ボタンを示しています。
プロジェクト フォルダーで、 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 オブジェクトに適用します。
ToListAsync オブジェクトに対して IQueryable メソッドが呼び出されると、要求されたページのみを含むListが返されます。
HasPreviousPageプロパティとHasNextPageプロパティを使用して、[前へ] および [次へ] ページング ボタンを有効または無効にすることができます。
コンストラクターは非同期コードを実行できないので、コンストラクターの代わりに 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 になります。 ページング リンクが選択されている場合、ページ変数には表示するページ番号が含まれます。
ViewDataという名前のCurrentSort要素は、ビューに現在の並べ替え順序を提供します。 ページング中も並べ替え順序が同じになるように、この値をページング リンクに含める必要があります。
ViewDataという名前のCurrentFilter要素は、ビューに現在のフィルター文字列を提供します。 ページング中にフィルター設定を維持するには、この値をページング リンクに含める必要があります。 ページが表示されたら、テキスト ボックスに値を復元する必要があります。
ページング中に検索文字列が変更された場合、新しいフィルターによって異なるデータが表示される可能性があるため、ページを 1 にリセットする必要があります。 テキスト ボックスに値を入力し、ユーザーが [送信] を選択すると、検索文字列が変更されます。 このシナリオでは、 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>
<label>Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" /></label>
<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 ステートメントは、ビューが PaginatedList<T> オブジェクトの代わりに List<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>
アプリを実行し、[ 学生 ] ページに移動します。
異なる並べ替え順序でページング リンクを選択し、ページングが機能することを確認します。 検索文字列を入力し、もう一度ページングを試して、ページングが並べ替えとフィルター処理でも正しく機能することを確認します。
About ページを作成する
Contoso University Web サイトの[概要]ページでは、各入学日に登録されている学生の数が表示されます。 この機能には、グループ化とグループに対する単純な計算が必要です。 この動作をサポートするには、次のタスクを実行します。
- ビューに渡す必要があるデータのビュー モデル クラスを作成します。
-
Aboutコントローラーに Home メソッドを作成します。 - 「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ステートメントを追加します。
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 格納します。
概要ビューを作成する
次のコードを 使用して 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>
アプリを実行し、[ バージョン情報 ] ページに移動します。 登録の日付ごとの学生の数が、テーブルに表示されます。
コードを取得する
次のステップ
ASP.NET Core