ASP.NET Core 3.0 Razor Pages 事始め(7) - Viewの変更とコンカレンシー例外処理


## 記事目次

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)の続きです。

---

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

今回は公式チュートリアルの[ASP.NET Core アプリで生成済みページを更新する](https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/razor-pages/da1?view=aspnetcore-3.0)に沿って進めていこうと思います。



## ページの見た目を変更する

まずは、Indexページの見た目の変更です。

チュートリアルでは、表のヘッダーの表示と、価格の表示を変更していますが、せっかくなので日本語で表示させようと思います。

まずは、Models/Movie.csを開いて、各プロパティに属性を追加します。

```c#
   public class Movie {
        public int ID { get; set; }

        [Display(Name ="タイトル")]
        public string Title { get; set; }

        [DataType(DataType.Date)]
        [Display(Name = "リリース日")]
        public DateTime ReleaseDate { get; set; }

        [Display(Name = "ジャンル")]
        public string Genre { get; set; }

        [Display(Name = "価格")]
        [DisplayFormat(DataFormatString ="{0:#,0}")]
        public decimal Price { get; set; }
    }
```

`[Display]`属性は、項目の名前として表示する文字列を指定します。Indexページでは、表のヘッダー部にこの文字列が表示されます。

`[DisplayFormat]`属性は、フォーマットの書式を指定します。チュートリアルでは、

```c#
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
```

のように属性を追加する例が載っていますが、ここでは価格が円であると考え、この属性は取りました。
それと、初期データも、小数点を取って、適当な値に直しておきました。

そういう意味では、`decimal`ではなく、`int` に変更したほうが良いのかもしれませんが、またマイグレーションやるのも面倒なので、このままにします。

ついでに、`Edit | Details | Delete` と `Create New` の部分も日本語に変更します。

では、テーブルのデータは全部消してから、再度アプリを起動し直します。

![スクリーンショット 2019-11-10 21.28.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/a025ee48-4fe1-97be-8e7b-89e16543de94.png "スクリーンショット 2019-11-10 21.28.41.png")

![スクリーンショット 2019-11-10 21.31.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/1a1995a0-a4bd-7226-760c-609924492651.png "スクリーンショット 2019-11-10 21.31.25.png")


OKのようです。

Indexページだけではなく、Editページなどほかのページも項目のラベルが日本語表記に変わりました。


変更したIndex.cshtmlもいちおう以下に示しておきます。


```html
@page
@model RazorPagesMovie.Pages.Movies.IndexModel

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

<h1>Index</h1>

<p>
    <a asp-page="Create">新規追加</a>
</p>
<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-page="./Edit" asp-route-id="@item.ID">編集</a> |
                <a asp-page="./Details" asp-route-id="@item.ID">詳細</a> |
                <a asp-page="./Delete" asp-route-id="@item.ID">削除</a>
            </td>
        </tr>
}
    </tbody>
</table>

```

それと、Chromeで動かすと、「このページを翻訳しますか」と聞いてくるのがうっとおしいので(Macでは大抵Safariで動かしてますが)、_Layout.cshtml を以下のように変更しました。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    …
    
```


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

前回も書いたと思うけど、再度、Index.cshtmlで使われているアンカー タグヘルパーを見てみます。

```html
<a asp-page="./Edit" asp-route-id="@item.ID">編集</a> |
<a asp-page="./Details" asp-route-id="@item.ID">詳細</a> |
<a asp-page="./Delete" asp-route-id="@item.ID">削除</a>
```

このタグヘルパーは、以下のようなHTMLに変換されます。

```html
<a href="/Movies/Edit?id=3">編集</a> |
<a href="/Movies/Details?id=3">詳細</a> |
<a href="/Movies/Delete?id=3">削除</a>
```

`href`属性が `asp-page` と `asp-route-id`から生成されているのが分かります。

`id=3`の `3`は、Movie.ID の値で、それぞれの行によって異なります。

## @pageディレクティブの変更

Edit.cshtml / Delete.cshtml / Details.cshtml の先頭行の

```
@page
```

を以下のように変更します。

```
@page "{id:int}"
```

すると、Indexページのアンカータグヘルパーの部分が、以下のようなHTMLに変換されるようになります。


```html
<a href="/Movies/Edit/3">編集</a> |
<a href="/Movies/Details/3">詳細</a> |
<a href="/Movies/Delete/3">削除</a>
```

Index.cshtml側は何も変更していないのに、出力されるHTMLが変わりました。
ちょっと、驚きですね。


でも、これだと、

```
https://localhost:5001/Movies/details
```

のように、idの値を省略すると、 `OnGetAsync` が呼び出される前に、404がブラウザに返ってしまいます。

idを省略可能にするには、３つの cshtmlを

```
@page "{id:int?}"

のように変更します。

こうすることで、 `OnGetAsync`が呼び出されます。
もちろん、

```c#
public async Task<IActionResult> OnPostAsync(int? id)
```

と指定していますから、id には、nullが渡ってきます。

これで、プログラムコード側で、省略された場合の動作を指定できるようになります。まあ、このアプリの場合はidを省略可能にする必要性はないですが、あくまでも実験ということで。


## 楽観的同時実行制御

チュートリアルでは、「コンカレンシーの例外処理」という用語を使っていますね。

Edit.cshtml.cs 内のデータを更新しているメソッド `OnPostAsync`を抜きだしてみます。

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

        _context.Attach(Movie).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(Movie.ID))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

```


`DbUpdateConcurrencyException`例外をキャッチするコードがあり、これが、コンカレンシーの例外処理です。

試しに、ブラウザを２つ立ち上げて、同じMovieに対して、片方では編集ページ、もう片方では削除ページを開き、２つめのページで削除してから、編集ページでデータを更新すると、`DbUpdateConcurrencyException`例外が発生します。

デバッグでブレークポイントを設定すると、それを確かめることができます。

![スクリーンショット 2019-11-04 13.12.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/b31dd67b-a795-15a3-6bc9-9144b1541825.png "スクリーンショット 2019-11-04 13.12.38.png")


ちなみに、2つのページで編集を開き、別の値で更新した時には、`DbUpdateConcurrencyException`例外が発生しませんでした。ASP.NET MVCと同様、rowversionカラムが必要みたいです。SQLiteはサポートしているのかな？

## モデルバインディング

### `OnGetAsync` メソッド

もう一度、Edit.cshtml.cs を見てみます。

 `OnGetAsync` メソッドでは、最後に
 
```c#
return Page();
```             

とすることで、Pages/Movies/Edit.cshtml Razor ページをレンダリングします。 Edit.cshtml ファイルでは

```html
@model RazorPagesMovie.Pages.Movies.EditModel
```

の行があるので、EditModelオブジェクトが使用できるようになります。あとは、Razor構文を使って、モデルの値をHTMLとバインドしていきます。


### `OnPostAsync`メソッド

 `EditModel`クラスには、

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

というプロパティがあります。これが、ビューとバインドするデータになります。
この`[BindProperty]`属性を付けることで、クライアントから送信されてきたデータが、Movieプロパティにバインドされます。

`OnPostAsync`メソッドの中に入ってきたときには、既に、MovieプロパティにはPOSTされた値が設定されているので、このサンプルでは、これをそのまま

```c#
_context.Attach(Movie).State = EntityState.Modified;
```

とすることで、DbContextに、Movieオブジェクトをアタッチして、状態を変更済みにしています。

この後で、

```c#
await _context.SaveChangesAsync();
```

とすれば、DBが更新されます。

なお、モデルにサーバー側で検知された検証エラーがあれば、`ModelState.IsValid`の値は、`false`になっています。