パート 7、ASP.NET Core MVC アプリへの検索の追加
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
作成者: Rick Anderson
このセクションでは、検索機能を Index
アクション メソッドに追加して、ジャンルまたは名前でムービーを検索できるようにします。
次のコードを使用して、Controllers/MoviesController.cs
内で見つかった Index
メソッドを更新します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Index
アクション メソッドの次の行により、ムービーを選択する LINQ クエリが作成されます。
var movies = from m in _context.Movie
select m;
このクエリは、この時点では "定義されるのみ" であり、データベースに対して実行されているわけではありません。
searchString
パラメーターに文字列が含まれる場合、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
上の s => s.Title!.ToUpper().Contains(searchString.ToUpper())
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、Where メソッドや Contains
(上のコードで使用されています) など、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
、Contains
、OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、ToListAsync
メソッドが呼び出されるまで、式の評価が延期されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
Note
Contains メソッドは C# コードではなく、データベースで実行されます。 クエリの大文字と小文字の区別は、データベースや照合順序に依存します。 SQL Server では、Contains
は大文字/小文字の区別がない SQL LIKE にマッピングされます。 既定の照合順序での SQLite は、クエリに応じて、大文字と小文字を区別する場合と区別 "しない" 場合が混在します。 大文字と小文字を区別しない SQLite クエリの作成については、次を参照してください。
/Movies/Index
に移動します。 ?searchString=Ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために Index
メソッドの署名を変更すると、id
パラメーターは、Program.cs
で設定されている既定ルートの省略可能な {id}
プレースホルダーと一致するようになります。
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
パラメーターを id
に変更します。searchString
がすべて id
に変更されます。
上記の Index
メソッド:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
id
パラメーターで更新された Index
メソッド:
public async Task<IActionResult> Index(string id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
}
return View(await movies.ToListAsync());
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、ここでは UI 要素を追加して、ムービーをフィルターできるようにします。 ルート バインドされた ID
パラメーターを渡す方法をテストするために Index
メソッドの署名を変更した場合は、searchString
という名前のパラメーターを受け取るように署名を元に戻します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Views/Movies/Index.cshtml
ファイルを開き、以下の強調表示されている <form>
マークアップを追加します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
HTML <form>
タグではフォーム タグ ヘルパーが使用されるため、フォームを送信するときに、フィルター文字列がムービー コントローラーの Index
アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。
予想どおり、Index
メソッドの [HttpPost]
オーバーロードはありません。 メソッドではデータをフィルターするだけで、アプリの状態を変更しないため、オーバーロードは必要ありません。
以下の [HttpPost] Index
メソッドを追加できます。
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
notUsed
パラメーターは、Index
メソッドのオーバーロードを作成するために使用されます。 これについては、チュートリアルの後半で説明します。
このメソッドを追加すると、アクション呼び出し元が [HttpPost] Index
メソッドと一致し、[HttpPost] Index
メソッドが以下のイメージのように実行されます。
ただし、この [HttpPost]
バージョンの Index
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。
次の図は、[ネットワーク] と [ヘッダー] タブが選択されている Chrome ブラウザーの開発者ツールを示しています。
[ネットワーク] および [ペイロード] タブが選択され、フォーム データが表示されます。
要求本文に検索パラメーターと XSRF トークンが表示されています。 なお、前述のチュートリアルで説明したように、フォーム タグ ヘルパーでは XSRF 偽造防止トークンが生成されます。 ここではデータを変更しないため、コントローラー メソッドでトークンを検証する必要はありません。
検索パラメーターが URL ではなく、要求本文にあるため、その検索情報をキャプチャして、ブックマークしたり、他のユーザーと共有したりすることはできません。 この問題を解決するには、Views/Movies/Index.cshtml
ファイルに存在する要求が form
タグの HTTP GET
であることを指定します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
ここで検索を送信すると、URL に検索クエリ文字列が含まれます。 HttpPost Index
メソッドがある場合でも、検索時には HttpGet Index
アクション メソッドにも移動します。
ジャンルによる検索の追加
次の MovieGenreViewModel
クラスを Models フォルダーに追加します。
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
ムービージャンルのビュー モデルには以下が含まれます。
- ムービーのリスト。
- ジャンルのリストを含む
SelectList
。 これにより、ユーザーは一覧からジャンルを選択できます。 - 選択されたジャンルを含む、
MovieGenre
。 - ユーザーが検索テキスト ボックスに入力したテキストが含まれる
SearchString
。
MoviesController.cs
の Index
メソッドを次のコードに置き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
次のコードは、データベースからすべてのジャンルを取得する LINQ
クエリです。
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
ジャンルの SelectList
は、個々のジャンルを投影して作成します (選択リストでジャンルが重複しないようにします)。
ユーザーが項目を検索すると、検索値が検索ボックスに保持されます。
インデックス ビューへのジャンルによる検索の追加
次のように、Views/Movies/ で見つかった Index.cshtml
を更新します。
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</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>
次の HTML ヘルパーで使用されるラムダ式を確認します。
@Html.DisplayNameFor(model => model.Movies![0].Title)
上のコードでは、DisplayNameFor
HTML ヘルパーは、ラムダ式で参照される Title
プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、model
、model.Movies
、または model.Movies[0]
が null
または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title)
など)、モデルのプロパティ値が評価されます。 model.Movies
の後の !
は null 免除演算子です。これは、Movies
が null ではないことを宣言するために使用されます。
ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。
このセクションでは、検索機能を Index
アクション メソッドに追加して、ジャンルまたは名前でムービーを検索できるようにします。
次のコードを使用して、Controllers/MoviesController.cs
内で見つかった Index
メソッドを更新します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Index
アクション メソッドの次の行により、ムービーを選択する LINQ クエリが作成されます。
var movies = from m in _context.Movie
select m;
このクエリは、この時点では "定義されるのみ" であり、データベースに対して実行されているわけではありません。
searchString
パラメーターに文字列が含まれる場合、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
上の s => s.Title!.ToUpper().Contains(searchString.ToUpper())
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、Where メソッドや Contains
(上のコードで使用されています) など、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
、Contains
、OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、ToListAsync
メソッドが呼び出されるまで、式の評価が延期されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
Note
Contains メソッドは C# コードではなく、データベースで実行されます。 クエリの大文字と小文字の区別は、データベースや照合順序に依存します。 SQL Server では、Contains
は大文字/小文字の区別がない SQL LIKE にマッピングされます。 既定の照合順序での SQLite は、クエリに応じて、大文字と小文字を区別する場合と区別 "しない" 場合が混在します。 大文字と小文字を区別しない SQLite クエリの作成については、次を参照してください。
/Movies/Index
に移動します。 ?searchString=Ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために Index
メソッドの署名を変更すると、id
パラメーターは、Program.cs
で設定されている既定ルートの省略可能な {id}
プレースホルダーと一致するようになります。
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
パラメーターを id
に変更します。searchString
がすべて id
に変更されます。
上記の Index
メソッド:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
id
パラメーターで更新された Index
メソッド:
public async Task<IActionResult> Index(string id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
}
return View(await movies.ToListAsync());
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、ここでは UI 要素を追加して、ムービーをフィルターできるようにします。 ルート バインドされた ID
パラメーターを渡す方法をテストするために Index
メソッドの署名を変更した場合は、searchString
という名前のパラメーターを受け取るように署名を元に戻します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Views/Movies/Index.cshtml
ファイルを開き、以下の強調表示されている <form>
マークアップを追加します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
HTML <form>
タグではフォーム タグ ヘルパーが使用されるため、フォームを送信するときに、フィルター文字列がムービー コントローラーの Index
アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。
予想どおり、Index
メソッドの [HttpPost]
オーバーロードはありません。 メソッドではデータをフィルターするだけで、アプリの状態を変更しないため、オーバーロードは必要ありません。
以下の [HttpPost] Index
メソッドを追加できます。
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
notUsed
パラメーターは、Index
メソッドのオーバーロードを作成するために使用されます。 これについては、チュートリアルの後半で説明します。
このメソッドを追加すると、アクション呼び出し元が [HttpPost] Index
メソッドと一致し、[HttpPost] Index
メソッドが以下のイメージのように実行されます。
ただし、この [HttpPost]
バージョンの Index
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。
要求本文に検索パラメーターと XSRF トークンが表示されています。 なお、前述のチュートリアルで説明したように、フォーム タグ ヘルパーでは XSRF 偽造防止トークンが生成されます。 ここではデータを変更しないため、コントローラー メソッドでトークンを検証する必要はありません。
検索パラメーターが URL ではなく、要求本文にあるため、その検索情報をキャプチャして、ブックマークしたり、他のユーザーと共有したりすることはできません。 この問題を解決するには、Views/Movies/Index.cshtml
ファイルに存在する要求が HTTP GET
であることを指定します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
ここで検索を送信すると、URL に検索クエリ文字列が含まれます。 HttpPost Index
メソッドがある場合でも、検索時には HttpGet Index
アクション メソッドにも移動します。
次のマークアップは form
タグの変更を示しています。
<form asp-controller="Movies" asp-action="Index" method="get">
ジャンルによる検索の追加
次の MovieGenreViewModel
クラスを Models フォルダーに追加します。
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
ムービージャンルのビュー モデルには以下が含まれます。
- ムービーのリスト。
- ジャンルのリストを含む
SelectList
。 これにより、ユーザーは一覧からジャンルを選択できます。 - 選択されたジャンルを含む、
MovieGenre
。 - ユーザーが検索テキスト ボックスに入力したテキストが含まれる
SearchString
。
MoviesController.cs
の Index
メソッドを次のコードに置き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
次のコードは、データベースからすべてのジャンルを取得する LINQ
クエリです。
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
ジャンルの SelectList
は、個々のジャンルを投影して作成します (選択リストでジャンルが重複しないようにします)。
ユーザーが項目を検索すると、検索値が検索ボックスに保持されます。
インデックス ビューへのジャンルによる検索の追加
次のように、Views/Movies/ で見つかった Index.cshtml
を更新します。
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</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>
次の HTML ヘルパーで使用されるラムダ式を確認します。
@Html.DisplayNameFor(model => model.Movies![0].Title)
上のコードでは、DisplayNameFor
HTML ヘルパーは、ラムダ式で参照される Title
プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、model
、model.Movies
、または model.Movies[0]
が null
または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title)
など)、モデルのプロパティ値が評価されます。 model.Movies
の後の !
は null 免除演算子です。これは、Movies
が null ではないことを宣言するために使用されます。
ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。
このセクションでは、検索機能を Index
アクション メソッドに追加して、ジャンルまたは名前でムービーを検索できるようにします。
次のコードを使用して、Controllers/MoviesController.cs
内で見つかった Index
メソッドを更新します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Index
アクション メソッドの次の行により、ムービーを選択する LINQ クエリが作成されます。
var movies = from m in _context.Movie
select m;
このクエリは、この時点では "定義されるのみ" であり、データベースに対して実行されているわけではありません。
searchString
パラメーターに文字列が含まれる場合、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
上の s => s.Title!.ToUpper().Contains(searchString.ToUpper())
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、Where メソッドや Contains
(上のコードで使用されています) など、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
、Contains
、OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、ToListAsync
メソッドが呼び出されるまで、式の評価が延期されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
Note
Contains メソッドは C# コードではなく、データベースで実行されます。 クエリの大文字と小文字の区別は、データベースや照合順序に依存します。 SQL Server では、Contains
は大文字/小文字の区別がない SQL LIKE にマッピングされます。 既定の照合順序での SQLite は、クエリに応じて、大文字と小文字を区別する場合と区別 "しない" 場合が混在します。 大文字と小文字を区別しない SQLite クエリの作成については、次を参照してください。
/Movies/Index
に移動します。 ?searchString=Ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために Index
メソッドの署名を変更すると、id
パラメーターは、Program.cs
で設定されている既定ルートの省略可能な {id}
プレースホルダーと一致するようになります。
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
パラメーターを id
に変更します。searchString
がすべて id
に変更されます。
上記の Index
メソッド:
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
id
パラメーターで更新された Index
メソッド:
public async Task<IActionResult> Index(string id)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(id.ToUpper()));
}
return View(await movies.ToListAsync());
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、ここでは UI 要素を追加して、ムービーをフィルターできるようにします。 ルート バインドされた ID
パラメーターを渡す方法をテストするために Index
メソッドの署名を変更した場合は、searchString
という名前のパラメーターを受け取るように署名を元に戻します。
public async Task<IActionResult> Index(string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
return View(await movies.ToListAsync());
}
Views/Movies/Index.cshtml
ファイルを開き、以下の強調表示されている <form>
マークアップを追加します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
HTML <form>
タグではフォーム タグ ヘルパーが使用されるため、フォームを送信するときに、フィルター文字列がムービー コントローラーの Index
アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。
予想どおり、Index
メソッドの [HttpPost]
オーバーロードはありません。 メソッドではデータをフィルターするだけで、アプリの状態を変更しないため、オーバーロードは必要ありません。
以下の [HttpPost] Index
メソッドを追加できます。
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
notUsed
パラメーターは、Index
メソッドのオーバーロードを作成するために使用されます。 これについては、チュートリアルの後半で説明します。
このメソッドを追加すると、アクション呼び出し元が [HttpPost] Index
メソッドと一致し、[HttpPost] Index
メソッドが以下のイメージのように実行されます。
ただし、この [HttpPost]
バージョンの Index
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。
要求本文に検索パラメーターと XSRF トークンが表示されています。 なお、前述のチュートリアルで説明したように、フォーム タグ ヘルパーでは XSRF 偽造防止トークンが生成されます。 ここではデータを変更しないため、コントローラー メソッドでトークンを検証する必要はありません。
検索パラメーターが URL ではなく、要求本文にあるため、その検索情報をキャプチャして、ブックマークしたり、他のユーザーと共有したりすることはできません。 この問題を解決するには、Views/Movies/Index.cshtml
ファイルに存在する要求が HTTP GET
であることを指定します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
ここで検索を送信すると、URL に検索クエリ文字列が含まれます。 HttpPost Index
メソッドがある場合でも、検索時には HttpGet Index
アクション メソッドにも移動します。
次のマークアップは form
タグの変更を示しています。
<form asp-controller="Movies" asp-action="Index" method="get">
ジャンルによる検索の追加
次の MovieGenreViewModel
クラスを Models フォルダーに追加します。
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models;
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
ムービージャンルのビュー モデルには以下が含まれます。
- ムービーのリスト。
- ジャンルのリストを含む
SelectList
。 これにより、ユーザーは一覧からジャンルを選択できます。 - 選択されたジャンルを含む、
MovieGenre
。 - ユーザーが検索テキスト ボックスに入力したテキストが含まれる
SearchString
。
MoviesController.cs
の Index
メソッドを次のコードに置き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
if (_context.Movie == null)
{
return Problem("Entity set 'MvcMovieContext.Movie' is null.");
}
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.ToUpper().Contains(searchString.ToUpper()));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
次のコードは、データベースからすべてのジャンルを取得する LINQ
クエリです。
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
ジャンルの SelectList
は、個々のジャンルを投影して作成します (選択リストでジャンルが重複しないようにします)。
ユーザーが項目を検索すると、検索値が検索ボックスに保持されます。
インデックス ビューへのジャンルによる検索の追加
次のように、Views/Movies/ で見つかった Index.cshtml
を更新します。
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies![0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies!)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</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>
次の HTML ヘルパーで使用されるラムダ式を確認します。
@Html.DisplayNameFor(model => model.Movies![0].Title)
上のコードでは、DisplayNameFor
HTML ヘルパーは、ラムダ式で参照される Title
プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、model
、model.Movies
、または model.Movies[0]
が null
または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title)
など)、モデルのプロパティ値が評価されます。 model.Movies
の後の !
は null 免除演算子です。これは、Movies
が null ではないことを宣言するために使用されます。
ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。
このセクションでは、検索機能を Index
アクション メソッドに追加して、ジャンルまたは名前でムービーを検索できるようにします。
次のコードを使用して、Controllers/MoviesController.cs
内で見つかった Index
メソッドを更新します。
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Index
アクション メソッドの最初の行により、ムービーを選択する LINQ クエリが作成されます。
var movies = from m in _context.Movie
select m;
このクエリはこの時点では定義されるだけで、データベースに対して実行されていません。
searchString
パラメーターに文字列が含まれる場合、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
上の s => s.Title!.Contains(searchString)
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、Where メソッドや Contains
(上のコードで使用されています) など、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
、Contains
、OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、ToListAsync
メソッドが呼び出されるまで、式の評価が延期されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
メモ:Contains メソッドは、上記の C# コードではなく、データベースで実行されます。 クエリの大文字と小文字の区別は、データベースや照合順序に依存します。 SQL Server では、Contains
は大文字/小文字の区別がない SQL LIKE にマッピングされます。 SQLite では、既定の照合順序で、大文字と小文字が区別されます。
/Movies/Index
に移動します。 ?searchString=Ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために Index
メソッドの署名を変更すると、id
パラメーターは、Program.cs
で設定されている既定ルートの省略可能な {id}
プレースホルダーと一致するようになります。
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
パラメーターを id
に変更します。searchString
がすべて id
に変更されます。
上記の Index
メソッド:
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
id
パラメーターで更新された Index
メソッド:
public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title!.Contains(id));
}
return View(await movies.ToListAsync());
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、ここでは UI 要素を追加して、ムービーをフィルターできるようにします。 ルート バインドされた ID
パラメーターを渡す方法をテストするために Index
メソッドの署名を変更した場合は、searchString
という名前のパラメーターを受け取るように署名を元に戻します。
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Views/Movies/Index.cshtml
ファイルを開き、以下の強調表示されている <form>
マークアップを追加します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
HTML <form>
タグではフォーム タグ ヘルパーが使用されるため、フォームを送信するときに、フィルター文字列がムービー コントローラーの Index
アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。
予想どおり、Index
メソッドの [HttpPost]
オーバーロードはありません。 メソッドではデータをフィルターするだけで、アプリの状態を変更しないため、オーバーロードは必要ありません。
以下の [HttpPost] Index
メソッドを追加できます。
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
notUsed
パラメーターは、Index
メソッドのオーバーロードを作成するために使用されます。 これについては、チュートリアルの後半で説明します。
このメソッドを追加すると、アクション呼び出し元が [HttpPost] Index
メソッドと一致し、[HttpPost] Index
メソッドが以下のイメージのように実行されます。
ただし、この [HttpPost]
バージョンの Index
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。
要求本文に検索パラメーターと XSRF トークンが表示されています。 なお、前述のチュートリアルで説明したように、フォーム タグ ヘルパーでは XSRF 偽造防止トークンが生成されます。 ここではデータを変更しないため、コントローラー メソッドでトークンを検証する必要はありません。
検索パラメーターが URL ではなく、要求本文にあるため、その検索情報をキャプチャして、ブックマークしたり、他のユーザーと共有したりすることはできません。 この問題を解決するには、Views/Movies/Index.cshtml
ファイルに存在する要求が HTTP GET
であることを指定します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
ここで検索を送信すると、URL に検索クエリ文字列が含まれます。 HttpPost Index
メソッドがある場合でも、検索時には HttpGet Index
アクション メソッドにも移動します。
次のマークアップは form
タグの変更を示しています。
<form asp-controller="Movies" asp-action="Index" method="get">
ジャンルによる検索の追加
次の MovieGenreViewModel
クラスを Models フォルダーに追加します。
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie>? Movies { get; set; }
public SelectList? Genres { get; set; }
public string? MovieGenre { get; set; }
public string? SearchString { get; set; }
}
}
ムービージャンルのビュー モデルには以下が含まれます。
- ムービーのリスト。
- ジャンルのリストを含む
SelectList
。 これにより、ユーザーは一覧からジャンルを選択できます。 - 選択されたジャンルを含む、
MovieGenre
。 - ユーザーが検索テキスト ボックスに入力したテキストが含まれる
SearchString
。
MoviesController.cs
の Index
メソッドを次のコードに置き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title!.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
次のコードは、データベースからすべてのジャンルを取得する LINQ
クエリです。
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
ジャンルの SelectList
は、個々のジャンルを投影して作成します (選択リストでジャンルが重複しないようにします)。
ユーザーが項目を検索すると、検索値が検索ボックスに保持されます。
インデックス ビューへのジャンルによる検索の追加
次のように、Views/Movies/ で見つかった Index.cshtml
を更新します。
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</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>
次の HTML ヘルパーで使用されるラムダ式を確認します。
@Html.DisplayNameFor(model => model.Movies[0].Title)
上のコードでは、DisplayNameFor
HTML ヘルパーは、ラムダ式で参照される Title
プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、model
、model.Movies
、または model.Movies[0]
が null
または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title)
など)、モデルのプロパティ値が評価されます。
ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。
このセクションでは、検索機能を Index
アクション メソッドに追加して、ジャンルまたは名前でムービーを検索できるようにします。
次のコードを使用して、Controllers/MoviesController.cs
内で見つかった Index
メソッドを更新します。
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Index
アクション メソッドの最初の行により、ムービーを選択する LINQ クエリが作成されます。
var movies = from m in _context.Movie
select m;
このクエリはこの時点では定義されるだけで、データベースに対して実行されていません。
searchString
パラメーターに文字列が含まれる場合、検索文字列の値でフィルターするようにムービー クエリが変更されます。
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
上の s => s.Title.Contains()
コードはラムダ式です。 ラムダは、メソッド ベースの LINQ クエリで、Where メソッドや Contains
(上のコードで使用されています) など、標準クエリ演算子メソッドの引数として使用されます。 LINQ クエリは、Where
、Contains
、OrderBy
などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、ToListAsync
メソッドが呼び出されるまで、式の評価が延期されます。 クエリの遅延実行の詳細については、「クエリの実行」を参照してください。
メモ:Contains メソッドは、上記の C# コードではなく、データベースで実行されます。 クエリの大文字と小文字の区別は、データベースや照合順序に依存します。 SQL Server では、Contains は大文字/小文字の区別がない SQL LIKE にマッピングされます。 SQLite では、既定の照合順序で、大文字と小文字が区別されます。
/Movies/Index
に移動します。 ?searchString=Ghost
などのクエリ文字列を URL に追加します。 フィルターされたムービーが表示されます。
id
という名前のパラメーターを使用するために Index
メソッドの署名を変更すると、id
パラメーターは、Startup.cs
で設定されている既定ルートの省略可能な {id}
プレースホルダーと一致するようになります。
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
パラメーターを id
に変更します。searchString
がすべて id
に変更されます。
上記の Index
メソッド:
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
id
パラメーターで更新された Index
メソッド:
public async Task<IActionResult> Index(string id)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(id))
{
movies = movies.Where(s => s.Title.Contains(id));
}
return View(await movies.ToListAsync());
}
これで、クエリ文字列の値ではなく、ルート データ (URL セグメント) として検索タイトルを渡すことができます。
ただし、ユーザーがムービーを検索するたびに URL の変更を求めることはできません。 そのため、ここでは UI 要素を追加して、ムービーをフィルターできるようにします。 ルート バインドされた ID
パラメーターを渡す方法をテストするために Index
メソッドの署名を変更した場合は、searchString
という名前のパラメーターを受け取るように署名を元に戻します。
public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(await movies.ToListAsync());
}
Views/Movies/Index.cshtml
ファイルを開き、以下の強調表示されている <form>
マークアップを追加します。
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
HTML <form>
タグではフォーム タグ ヘルパーが使用されるため、フォームを送信するときに、フィルター文字列がムービー コントローラーの Index
アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。
予想どおり、Index
メソッドの [HttpPost]
オーバーロードはありません。 メソッドではデータをフィルターするだけで、アプリの状態を変更しないため、オーバーロードは必要ありません。
以下の [HttpPost] Index
メソッドを追加できます。
[HttpPost]
public string Index(string searchString, bool notUsed)
{
return "From [HttpPost]Index: filter on " + searchString;
}
notUsed
パラメーターは、Index
メソッドのオーバーロードを作成するために使用されます。 これについては、チュートリアルの後半で説明します。
このメソッドを追加すると、アクション呼び出し元が [HttpPost] Index
メソッドと一致し、[HttpPost] Index
メソッドが以下のイメージのように実行されます。
ただし、この [HttpPost]
バージョンの Index
メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。
要求本文に検索パラメーターと XSRF トークンが表示されています。 なお、前述のチュートリアルで説明したように、フォーム タグ ヘルパーでは XSRF 偽造防止トークンが生成されます。 ここではデータを変更しないため、コントローラー メソッドでトークンを検証する必要はありません。
検索パラメーターが URL ではなく、要求本文にあるため、その検索情報をキャプチャして、ブックマークしたり、他のユーザーと共有したりすることはできません。 この問題を解決するには、Views/Movies/Index.cshtml
ファイルに存在する要求が HTTP GET
であることを指定します。
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<label>Title: <input type="text" name="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
ここで検索を送信すると、URL に検索クエリ文字列が含まれます。 HttpPost Index
メソッドがある場合でも、検索時には HttpGet Index
アクション メソッドにも移動します。
次のマークアップは form
タグの変更を示しています。
<form asp-controller="Movies" asp-action="Index" method="get">
ジャンルによる検索の追加
次の MovieGenreViewModel
クラスを Models フォルダーに追加します。
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> Movies { get; set; }
public SelectList Genres { get; set; }
public string MovieGenre { get; set; }
public string SearchString { get; set; }
}
}
ムービージャンルのビュー モデルには以下が含まれます。
- ムービーのリスト。
- ジャンルのリストを含む
SelectList
。 これにより、ユーザーは一覧からジャンルを選択できます。 - 選択されたジャンルを含む、
MovieGenre
。 - ユーザーが検索テキスト ボックスに入力したテキストが含まれる
SearchString
。
MoviesController.cs
の Index
メソッドを次のコードに置き換えます。
// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
var movies = from m in _context.Movie
select m;
if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}
var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};
return View(movieGenreVM);
}
次のコードは、データベースからすべてのジャンルを取得する LINQ
クエリです。
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;
ジャンルの SelectList
は、個々のジャンルを投影して作成します (選択リストでジャンルが重複しないようにします)。
ユーザーが項目を検索すると、検索値が検索ボックスに保持されます。
インデックス ビューへのジャンルによる検索の追加
次のように、Views/Movies/ で見つかった Index.cshtml
を更新します。
@model MvcMovie.Models.MovieGenreViewModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
<p>
<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
<label>Title: <input type="text" asp-for="SearchString" /></label>
<input type="submit" value="Filter" />
</p>
</form>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Genre)
</th>
<th>
@Html.DisplayNameFor(model => model.Movies[0].Price)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Movies)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</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>
次の HTML ヘルパーで使用されるラムダ式を確認します。
@Html.DisplayNameFor(model => model.Movies[0].Title)
上のコードでは、DisplayNameFor
HTML ヘルパーは、ラムダ式で参照される Title
プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、model
、model.Movies
、または model.Movies[0]
が null
または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title)
など)、モデルのプロパティ値が評価されます。
ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。
ASP.NET Core