ASP.NET Core 3.0 Razor Pages 事始め(8) - 検索機能の追加


## 記事目次

1. [ASP.NET Core 3.0 Razor Pages 事始め(1) - はじめてのRazor Pagesアプリケーション](https://qiita.com/gushwell/items/6d8b801923b639d9ea2e)
2. [ASP.NET Core 3.0 Razor Pages 事始め(2) - スキャフォールディングとDBマイグレーション](https://qiita.com/gushwell/items/7eb76edf7006c0f02be4)
3. [ASP.NET Core 3.0 Razor Pages 事始め(3) - マイグレーションのやり直しとURLルーティング](https://qiita.com/gushwell/items/7ebe70f0d6adfd10ae12)
4. [ASP.NET Core 3.0 Razor Pages 事始め(4) - ページモデルとページハンドラ](https://qiita.com/gushwell/items/1deac26aaed945cc8631)
5. [ASP.NET Core 3.0 Razor Pages 事始め(5) - Postページハンドラとタグヘルパー](https://qiita.com/gushwell/items/daf901d4d76301dadea7)
6. [ASP.NET Core 3.0 Razor Pages 事始め(6) - データベースに初期値を設定する](https://qiita.com/gushwell/items/f7be44e668a6e168fc61)
7. [ASP.NET Core 3.0 Razor Pages 事始め(7) - Viewの変更とコンカレンシー例外処理](https://qiita.com/gushwell/items/913e8e95a0a79949af3c)
8. [ASP.NET Core 3.0 Razor Pages 事始め(8) - 検索機能の追加](https://qiita.com/gushwell/items/134930aba1d181100547) <-- この記事
9. [ASP.NET Core 3.0 Razor Pages 事始め(9) - ページに新しいフィールドを追加する](https://qiita.com/gushwell/items/a330e592f76efda13c42)
10. [ASP.NET Core 3.0 Razor Pages 事始め(10) - 検証機能の追加](https://qiita.com/gushwell/items/12e180ef129ea6a05757)


---

[ASP.NET Core 3.0 Razor Pages 事始め(7)](https://qiita.com/gushwell/items/913e8e95a0a79949af3c)の続きです。

今回は公式チュートリアルの[ASP.NET Core Razor ページへの検索の追加](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/razor-pages/search?view=aspnetcore-3.0)に沿って進めていこうと思います。


## IndexModel へプロパティを追加

Indexページで検索機能を追加します。

まずは、IndexModelに、検索用のプロパティを追加します。

```c#
using Microsoft.AspNetCore.Mvc.Rendering;

…

    public class IndexModel : PageModel
    {
        private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

        public IndexModel(RazorPagesMovie.Models.RazorPagesMovieContext context)
        {
            _context = context;
        }

        public IList<Movie> Movies { get;set; }

        [BindProperty(SupportsGet = true)]
        public string SearchString { get; set; }

        // Requires using Microsoft.AspNetCore.Mvc.Rendering;
        public SelectList Genres { get; set; }

        [BindProperty(SupportsGet = true)]
        public string MovieGenre { get; set; }


        public async Task OnGetAsync()
        {
            Movies = await _context.Movies.ToListAsync();
        }
    }
```

追加したのは、以下の３つのプロパティです。

### SearchString

ユーザーが検索テキスト ボックスに入力した値がここに入ります。 
[BindProperty] 属性で修飾されています。これで、HTMLの同じ名前のフォーム要素（あるいはクエリ文字列）がこのプロパティにバインドされます。 `SupportsGet = true` は、GET 要求でのバインドで必要です。

### Genres

ジャンル一覧を表しています。 選択一覧に表示される項目として利用されます。`SelectList`クラスは `Microsoft.AspNetCore.Mvc.Rendering` 名前空間に定義されています。

### MovieGenre

ユーザーが選択したジャンルが入ります。`SearchString`プロパティと同様に、[BindProperty] 属性で修飾されています。

## IndexModel の OnGetAsync メソッド変更

次に、IndexModel の OnGetAsync メソッドを変更します。変更後のOnGetAsyncメソッドを示します。

```c#
    public async Task OnGetAsync()
    {
        var movies = _context.Movies as IQueryable<Movie>;
        if (!string.IsNullOrEmpty(SearchString)) {
            movies = movies.Where(s => s.Title.Contains(SearchString));
        }

        Movies = await movies.ToListAsync();
    }
```

この`OnGetAsync`メソッドが正しく動作するかを確認してみます。
F5キーでデバッグを開始します。

ブラウザが起動したら、Moviesページに移動します。

そして、`?searchString=Ghost` のクエリ文字列を URLに追加して実行してみます。

```
https://localhost:5001/movies?searchString=Ghost
```

たしかに、`Ghost`で絞り込めました。

![スクリーンショット 2019-11-12 21.32.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/0a4fbde9-af06-cc1c-bef1-fe94d463fabc.png "スクリーンショット 2019-11-12 21.32.12.png")


次に、デバッグを終了し、index.chtmlを開きます。
先頭の行を以下のように書き換えます。

```html
@page "{searchString?}"
```

これで、クエリ文字列の代わりに、URLのルートデータとして題名を指定できるようになります。これを**ルート制約**と言うようです。

これも試してみます。

```
https://localhost:5001/movies/Ghost
```

先ほどと同じ結果になりました。

## 検索用UIの追加

しかし、URLをユーザに入力してもらうわけにはいきません。
そのため、index.cshtmlに検索のためのUIを追加します。

先ほどのルート制約を削除します。

それから、index.cshtmlを開き、以下のように `<form>`要素を `<table>`要素の直前に追加します。

```html
<p>
    <a asp-page="Create">新規追加</a>
</p>

<form>
    <p>
        @Html.DisplayNameFor(model => model.Movies[0].Title):
        <input type="text" asp-for="SearchString" />
        <button type="submit">検索</button>
    </p>
</form>


<table class="table">
    <thead>
```    

`<form>`のデフォルトの methodの値は、"get"なので、検索ボタンを押せば、`OnGetAsync`メソッドが呼びだされます。

チュートリアルのページでは、"Title"と文字列リテラルを使っていましたが、ここでは、@Html.DisplayNameForを使うようにしてみました。

HTML的には、`<label>`を使ったほうが良いのかもしれませんね。でもその場合は、`SearchString`プロパティに、`[Display(Name ="タイトル")]`という属性を付加する必要がありますね。

では、テストしてみます。
うまく動いているようです。

![スクリーンショット 2019-11-12 21.36.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/c43b08ee-3746-2a5b-a289-5488e071acc5.png "スクリーンショット 2019-11-12 21.36.24.png")


検索フィールドを空にして、[検索]ボタンを押せば、すべてのタイトルが表示されます。



## ジャンルで検索

次にジャンルで検索する機能を追加します。

index.cshtml.csの `OnGetAsync` メソッドを次のように更新します。
なお、チュートリアルページのコードが僕の好みじゃないので書き換えています。

```c#
public async Task OnGetAsync() {
    // Moviesをフィルタリング
    var movies = _context.Movies as IQueryable<Movie>;

    if (!string.IsNullOrEmpty(SearchString)) {
        movies = movies.Where(s => s.Title.Contains(SearchString));
    }

    if (!string.IsNullOrEmpty(MovieGenre)) {
        movies = movies.Where(x => x.Genre == MovieGenre);
    }
    Movies = await movies.ToListAsync();

    // ジャンル一覧を生成
    var genreList = await _context.Movies.OrderBy(m => m.Genre)
                                    .Select(m => m.Genre)
                                    .Distinct()
                                    .ToListAsync();
    Genres = new SelectList(genreList);
}
```

このほうが分かりやすいと個人的には思います。

次に、Index.cshtmlの`<form>`要素を以下のように変更します。

```html
<form>
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        @Html.DisplayNameFor(model => model.Movies[0].Title):
        <input type="text" asp-for="SearchString" />
        <button type="submit">検索</button>
    </p>
</form>
```

へー、こんな風に、`<select>`要素内の`<option>`の一部を htmlに書くこともできるんですね。
この場合は、`asp-items`でバインドされた`<option>`は、

```
    <option value="">All</option>
```

の下に展開されるようです。以下展開された　select要素です。


```html
<select id="MovieGenre" name="MovieGenre">
    <option value="">All</option>
    <option>Comedy</option>
    <option>Romantic Comedy</option>
    <option>Western</option>
</select>
```

ちなみに、`All`を選んだときは、`All`ではなく、空文字列がサーバーに送られるように、`value=""` としています。


ビルドが通ったら、ジャンルまたはムービーのタイトル、あるいはその両方で検索して、正しく動作するかをテストします。

正しく動作しているようです。

![スクリーンショット 2019-11-12 21.42.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/a3967e19-d175-68de-8aa8-291fdb2b522d.png "スクリーンショット 2019-11-12 21.42.26.png")

![スクリーンショット 2019-11-12 21.43.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/f47cc6e0-eeed-722e-2177-36f789a9ab7a.png "スクリーンショット 2019-11-12 21.43.08.png")