ASP.NET Core 3.0 Razor Pages 事始め(5) - Postページハンドラとタグヘルパー


## 記事目次

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)
11. 前回[ASP.NET Core 3.0 Razor Pages 事始め(3)](https://qiita.com/gushwell/items/7ebe70f0d6adfd10ae12)の続きです。
12. [ASP.NET Core 3.0 Razor Pages 事始め(4)](https://qiita.com/gushwell/items/1deac26aaed945cc8631)の続きです。

---

なかなか先に進まないですが、ただ動かすだけではなく、コードを理解することが大切だと思うので、今回も、スキャフォールディングで作成されたソースコードを見ていきます。


## Create.cshtml.cs

このファイルには、以下に示すように `CreateModel`クラスが定義されています。

```c#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using RazorPagesMovie.Models;

namespace RazorPagesMovie.Pages_Movies
{
    public class CreateModel : PageModel
    {
        private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;

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

        public IActionResult OnGet()
        {
            return Page();
        }

        [BindProperty]
        public Movie Movie { get; set; }

        // To protect from overposting attacks, please enable the specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            _context.Movies.Add(Movie);
            await _context.SaveChangesAsync();

            return RedirectToPage("./Index");
        }
    }
}
```

### OnGetメソッド

Index.cshtmlと違い、同期メソッドとして定義されています。

```c#
public IActionResult OnGet()
{
    return Page();
}
```

これは、中で非同期メソッド呼ぶ必要がないからですね。

試しに、

```c#
public void OnGet() {
}
```

と書き換えて実行してみました。想像通り、正しく動作しました。

### OnPostAsync メソッド

`CreateModel`クラスには、もう一つのメソッドが定義されています。

```c#
public async Task<IActionResult> OnPostAsync()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _context.Movies.Add(Movie);
    await _context.SaveChangesAsync();

    return RedirectToPage("./Index");
}
```

Http Postに対応するメソッドです。

最初のif文は、旧来の ASP.NET MVCと同じですね。

```c#
if (!ModelState.IsValid)
{
    return Page();
}
```

これで、モデルの検証結果を判定し、エラーだったら、再度同じページを表示させています。

エラーがなかった場合は、

```c#
_context.Movies.Add(Movie);
await _context.SaveChangesAsync();
```            

で入力されたMovieデータを追加し、データベースを更新しています。

### BindProperty属性

上記`Addメソッド`の引数の`Movie`は、自分自身（CreateModelクラス内）に定義されたプロパティです。


```c#
[BindProperty]
public Movie Movie { get; set; }
```        

Webページで入力されPOSTされたデータは、このプロパティにバインドされます。
[BindProperty]属性が、バインドされるプロパティであることを示しています。

### RedirectToPageメソッド

最後に、

```c#
return RedirectToPage("./Index");
```

で、同一パスの、Indexページにリダイレクト（遷移）させています。


## Create.cshtml

Create.cshtmlについて見てみます。

```html
@page
@model RazorPagesMovie.Pages_Movies.CreateModel

@{
    ViewData["Title"] = "Create";
}

<h1>Create</h1>

<h4>Movie</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form method="post">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Movie.Title" class="control-label"></label>
                <input asp-for="Movie.Title" class="form-control" />
                <span asp-validation-for="Movie.Title" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.ReleaseDate" class="control-label"></label>
                <input asp-for="Movie.ReleaseDate" class="form-control" />
                <span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Genre" class="control-label"></label>
                <input asp-for="Movie.Genre" class="form-control" />
                <span asp-validation-for="Movie.Genre" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Movie.Price" class="control-label"></label>
                <input asp-for="Movie.Price" class="form-control" />
                <span asp-validation-for="Movie.Price" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

<div>
    <a asp-page="Index">Back to List</a>
</div>
```

これを見ると、スキャフォールディングエンジンは、IDプロパティは出力の対象外のようですね。

Create.cshtmlでは、Index.cshtmlでは使われていなかったタグヘルパーがいくつか使われています。

### formタグヘルパー

```html
<form method="post">
```

見た目は、HTMLの`<form>`タグそのものですが、自動的に偽造防止トークンが出力されます。
僕の環境だと、`</form>`閉じタグの直前に以下の隠しフィールドが挿入されてました。

```html
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8Kws0PPAm1NChsmiTNfRcsX11bkfDPchGc83DJyKlltDfES4-RRpQRGC1_kpkDISRPnKaeSbEBVxYPCqSppRFCM1PieCGNkLk_2u8oUEuBeguEpVQISizdTFjnuCRDIu-IGMQEDP7MNq6UJM57TBku8">
```


### label タグヘルパー

```html
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
```

このヘルパーは、以下のタグを生成します。

```html
<label class="control-label" for="Movie_ReleaseDate">ReleaseDate</label>
```

`asp-for`で、指定したモデルに該当する `for`属性と、ラベルのキャプションを生成します。`<label>`要素のコンテンツも自動で挿入されている点に注目です。

### input タグヘルパー

```html
<input asp-for="Movie.ReleaseDate" class="form-control" />
```

`asp-for`属性で指定したモデル（ここでは、`Movie.ReleaseDate`)の型や`DataAnnotation`属性の情報を使用して、適切な`<input>`要素を出力します。


```html
<input class="form-control" type="date" data-val="true" data-val-required="The ReleaseDate field is required." id="Movie_ReleaseDate" name="Movie.ReleaseDate" value="">
```

また、jQueryのクライアント検証に必要な属性も追加します。例えば、`data-val-required`は、必須属性であることを示しています。この属性値で設定したメッセージが、後述の検証タグヘルパー(`asp-validation-for`)の場所に表示されます。


### アンカータグヘルパー

```html
<a asp-page="Index">Back to List</a>
```    

アンカータグを生成するタグヘルパーです。`href` ではなく、`asp-page=`属性を指定します。ASP.NET Core MVCだと、`asp-controller`, `asp-action` ですが、Razorページの場合は、`asp-page`です。

この場合は、以下のHTMLが出力されます。

```html
<a href="/Movies">Back to List</a>
```    



### 検証タグヘルパー

```html
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
```

```html
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
```

この２つが、検証タグヘルパーです。`asp-validation-summary`, `asp-validation-for` 属性がそれを表します。検証エラーがあった時に、ここにエラーメッセージが表示されます。

`asp-validation-summary`は、検証エラーの概要を表示するために使用されます。

 `asp-validation-for` は、特定の入力項目に対する検証エラーメッセージを表示するために使用されます。

なお、`create.cshtml`の最後にある

```html
@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
```

は、クライアント検証に必要になります。これがないと、jQueryのクライアント検証が有効になりません。

具体的には、`_Layout.cshtml`内に以下の記述があるのですが、

```html
@RenderSection("Scripts", required: false)
```

この`@RenderSection` が書かれた場所に、`_ValidationScriptsPartial.cshtml`に書かれた内容が、挿入されることで、jQueryクライアント検証が有効になります。

### wwwroot フォルダ

jQueryの話題がすこし出てきたので、wwwrootフォルダの中ものぞいてみます。
このフォルダは、以下のようなフォルダ階層になっていました。

```
wwwroot
    ├─css
    ├─js
    └─lib
        ├─bootstrap
        │  └─dist
        │      ├─css
        │      └─js
        ├─jquery
        │  └─dist
        ├─jquery-validation
        │  └─dist
        └─jquery-validation-unobtrusive
```

アプリで作成した css, js ファイルは、それぞれ、css, js フォルダに置きます。libは、外部パッケージを置く場所のようです。

画像ファイルがある場合は、imagesフォルダを作成するのが標準のやり方のようです。


ちなみに、bootstrapのバージョンは v4.3.1  jQuery のバージョンは  v3.3.1 でした。


## その他のページ

Index, Create の２つのページについて見てきましたが、残りのページの特徴的な部分だけ見てみましょう。

### `asp-route-id`属性

以下は、Delete.cshtml.csの`OnGetAsync`メソッドです。

```c#
public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Movie = await _context.Movies.FirstOrDefaultAsync(m => m.ID == id);

    if (Movie == null)
    {
        return NotFound();
    }
    return Page();
}
```

引数 id を受け取っています。

index.cshtmlの以下のリンクがクリックされた時に、この上記メソッドが呼び出されます。

```html
<a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
```

`asp-route-id="@item.ID"`で指定した IDの値が、引数idに渡ることになります。

ちなみに、上記のタグヘルパーで、以下のような出力が得られます。

```html
<a href="/Movies/Delete?id=2">Delete</a>
```

この例では、仮引数idに、2が渡ってくることになります。

### return NotFound()

もし、idに該当する行がテーブルになければ、

```c#
return NotFound();
```

で、404がクライアントに返ります。

試しに、無理やり NotFound()を返してみたら、エラーページは返らないようです。以下、safariの開発者ツールで確認した画面です。


![スクリーンショット 2019-10-30 21.36.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/e8c1e5f0-db57-466d-6b23-782346154a24.png "スクリーンショット 2019-10-30 21.36.01.png")

たしかに、404が返っています。

でもこれではユーザは何が起こったかわからないので、何らかのカスタマイズが必要そうですね。これは後で調べます。


一通りページ関連のソースコードを見てみたので、次は、チュートリアルの「データベースの操作」に移ります。