パート 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 クエリは、WhereContainsOrderBy などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、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 セグメント) として検索タイトルを渡すことができます。

ghost という単語が URL に追加された索引ビュー。Ghostbusters と Ghostbusters 2 という 2 本のムービーからなるムービーリストが返されています。

ただし、ユーザーがムービーを検索するたびに 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 アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。

タイトル フィルター テキストボックスに ghost という単語が入力されたインデックス ビュー

予想どおり、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 メソッドが以下のイメージのように実行されます。

From HttpPost Index: filter on ghost というアプリケーション応答を示すブラウザー ウィンドウ

ただし、この [HttpPost] バージョンの Index メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。

次の図は、[ネットワーク][ヘッダー] タブが選択されている Chrome ブラウザーの開発者ツールを示しています。

searchString 値がゴーストの要求本文を示す Chrome ブラウザーの開発者ツールの [ネットワーク] および [ヘッダー] タブ

[ネットワーク] および [ペイロード] タブが選択され、フォーム データが表示されます。

フォーム データが表示されている 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 アクション メソッドにも移動します。

URL に searchString=ghost が表示されたブラウザー ウィンドウ。返された Ghostbusters および Ghostbusters 2 というムービーには ghost という単語が含まれています

ジャンルによる検索の追加

次の 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.csIndex メソッドを次のコードに置き換えます。

// 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 プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、modelmodel.Movies、または model.Movies[0]null または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title) など)、モデルのプロパティ値が評価されます。 model.Movies の後の !null 免除演算子です。これは、Movies が null ではないことを宣言するために使用されます。

ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。

https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 の結果を示すブラウザー ウィンドウ

このセクションでは、検索機能を 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 クエリは、WhereContainsOrderBy などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、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 セグメント) として検索タイトルを渡すことができます。

ghost という単語が URL に追加された索引ビュー。Ghostbusters と Ghostbusters 2 という 2 本のムービーからなるムービーリストが返されています。

ただし、ユーザーがムービーを検索するたびに 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 アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。

タイトル フィルター テキストボックスに ghost という単語が入力されたインデックス ビュー

予想どおり、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 メソッドが以下のイメージのように実行されます。

From HttpPost Index: filter on ghost というアプリケーション応答を示すブラウザー ウィンドウ

ただし、この [HttpPost] バージョンの Index メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。

searchString 値が ghost の要求本文を示す、Microsoft Edge の開発者ツールの [ネットワーク] タブ

要求本文に検索パラメーターと 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 アクション メソッドにも移動します。

URL に searchString=ghost が表示されたブラウザー ウィンドウ。返された Ghostbusters および Ghostbusters 2 というムービーには ghost という単語が含まれています

次のマークアップは 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.csIndex メソッドを次のコードに置き換えます。

// 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 プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、modelmodel.Movies、または model.Movies[0]null または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title) など)、モデルのプロパティ値が評価されます。 model.Movies の後の !null 免除演算子です。これは、Movies が null ではないことを宣言するために使用されます。

ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。

https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 の結果を示すブラウザー ウィンドウ

このセクションでは、検索機能を 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 クエリは、WhereContainsOrderBy などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、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 セグメント) として検索タイトルを渡すことができます。

ghost という単語が URL に追加された索引ビュー。Ghostbusters と Ghostbusters 2 という 2 本のムービーからなるムービーリストが返されています。

ただし、ユーザーがムービーを検索するたびに 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 アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。

タイトル フィルター テキストボックスに ghost という単語が入力されたインデックス ビュー

予想どおり、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 メソッドが以下のイメージのように実行されます。

From HttpPost Index: filter on ghost というアプリケーション応答を示すブラウザー ウィンドウ

ただし、この [HttpPost] バージョンの Index メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。

searchString 値が ghost の要求本文を示す、Microsoft Edge の開発者ツールの [ネットワーク] タブ

要求本文に検索パラメーターと 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 アクション メソッドにも移動します。

URL に searchString=ghost が表示されたブラウザー ウィンドウ。返された Ghostbusters および Ghostbusters 2 というムービーには ghost という単語が含まれています

次のマークアップは 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.csIndex メソッドを次のコードに置き換えます。

// 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 プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、modelmodel.Movies、または model.Movies[0]null または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title) など)、モデルのプロパティ値が評価されます。 model.Movies の後の !null 免除演算子です。これは、Movies が null ではないことを宣言するために使用されます。

ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。

https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 の結果を示すブラウザー ウィンドウ

このセクションでは、検索機能を 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 クエリは、WhereContainsOrderBy などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、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 セグメント) として検索タイトルを渡すことができます。

ghost という単語が URL に追加された索引ビュー。Ghostbusters と Ghostbusters 2 という 2 本のムービーからなるムービーリストが返されています。

ただし、ユーザーがムービーを検索するたびに 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 アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。

タイトル フィルター テキストボックスに ghost という単語が入力されたインデックス ビュー

予想どおり、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 メソッドが以下のイメージのように実行されます。

From HttpPost Index: filter on ghost というアプリケーション応答を示すブラウザー ウィンドウ

ただし、この [HttpPost] バージョンの Index メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。

searchString 値が ghost の要求本文を示す、Microsoft Edge の開発者ツールの [ネットワーク] タブ

要求本文に検索パラメーターと 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 アクション メソッドにも移動します。

URL に searchString=ghost が表示されたブラウザー ウィンドウ。返された Ghostbusters および Ghostbusters 2 というムービーには ghost という単語が含まれています

次のマークアップは 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.csIndex メソッドを次のコードに置き換えます。

// 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 プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、modelmodel.Movies、または model.Movies[0]null または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title) など)、モデルのプロパティ値が評価されます。

ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。

https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 の結果を示すブラウザー ウィンドウ

このセクションでは、検索機能を 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 クエリは、WhereContainsOrderBy などのメソッドの呼び出しで定義または変更されたときには実行されません。 クエリ実行は先送りされます。 つまり、その具体値が実際に繰り返されるか、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 セグメント) として検索タイトルを渡すことができます。

ghost という単語が URL に追加された索引ビュー。Ghostbusters と Ghostbusters 2 という 2 本のムービーからなるムービーリストが返されています。

ただし、ユーザーがムービーを検索するたびに 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 アクションに投稿されます。 変更内容を保存してから、フィルターをテストします。

タイトル フィルター テキストボックスに ghost という単語が入力されたインデックス ビュー

予想どおり、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 メソッドが以下のイメージのように実行されます。

From HttpPost Index: filter on ghost というアプリケーション応答を示すブラウザー ウィンドウ

ただし、この [HttpPost] バージョンの Index メソッドを追加しても、実装方法は制限されます。 たとえば、特定の検索をブックマークするか、友だちにリンクを送信し、友だちがそれをクリックしてムービーのフィルターされた同じリストを表示できるようにするとします。 HTTP POST 要求の URL は、GET 要求の URL (localhost:{PORT}/Movies/Index) と同じであり、URL には検索情報がないことに注意してください。 検索文字列情報は、フォーム フィールド値としてサーバーに送信されます。 ブラウザーの開発者ツールまたは優れた Fiddler ツールを使用して、これを確認できます。 次のイメージは、Chrome ブラウザーの開発者ツールを示しています。

searchString 値が ghost の要求本文を示す、Microsoft Edge の開発者ツールの [ネットワーク] タブ

要求本文に検索パラメーターと 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 アクション メソッドにも移動します。

URL に searchString=ghost が表示されたブラウザー ウィンドウ。返された Ghostbusters および Ghostbusters 2 というムービーには ghost という単語が含まれています

次のマークアップは 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.csIndex メソッドを次のコードに置き換えます。

// 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 プロパティを検査し、表示名を判別します。 ラムダ式は評価されるのではなく、検査されるため、modelmodel.Movies、または model.Movies[0]null または空である場合にアクセス違反が発生することはありません。 ラムダ式が評価される場合 (@Html.DisplayFor(modelItem => item.Title) など)、モデルのプロパティ値が評価されます。

ジャンルまたはムービーのタイトル、あるいはその両方で検索して、アプリをテストします。

https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2 の結果を示すブラウザー ウィンドウ