3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-10

記事目次

  1. ASP.NET Core 3.0 Razor Pages 事始め(1) - はじめてのRazor Pagesアプリケーション
  2. ASP.NET Core 3.0 Razor Pages 事始め(2) - スキャフォールディングとDBマイグレーション
  3. ASP.NET Core 3.0 Razor Pages 事始め(3) - マイグレーションのやり直しとURLルーティング
  4. ASP.NET Core 3.0 Razor Pages 事始め(4) - ページモデルとページハンドラ
  5. ASP.NET Core 3.0 Razor Pages 事始め(5) - Postページハンドラとタグヘルパー
  6. ASP.NET Core 3.0 Razor Pages 事始め(6) - データベースに初期値を設定する
  7. ASP.NET Core 3.0 Razor Pages 事始め(7) - Viewの変更とコンカレンシー例外処理 <-- この記事
  8. ASP.NET Core 3.0 Razor Pages 事始め(8) - 検索機能の追加
  9. ASP.NET Core 3.0 Razor Pages 事始め(9) - ページに新しいフィールドを追加する
  10. ASP.NET Core 3.0 Razor Pages 事始め(10) - 検証機能の追加
  11. 前回ASP.NET Core 3.0 Razor Pages 事始め(3)の続きです。
  12. ASP.NET Core 3.0 Razor Pages 事始め(4)の続きです。

ASP.NET Core 3.0 Razor Pages 事始め(6)の続きです。

今回は公式チュートリアルのASP.NET Core アプリで生成済みページを更新するに沿って進めていこうと思います。

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

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

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

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

   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]属性は、フォーマットの書式を指定します。チュートリアルでは、

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

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

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

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

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

スクリーンショット 2019-11-10 21.28.41.png

スクリーンショット 2019-11-10 21.31.25.png

OKのようです。

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

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

@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 を以下のように変更しました。

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

アンカー タグヘルパー

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

<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に変換されます。

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

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

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

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

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

@page

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

@page "{id:int}"

すると、Indexページのアンカータグヘルパーの部分が、以下のような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を省略可能にするには、3つの cshtmlを

@page "{id:int?}"

のように変更します。

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

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

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

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

楽観的同時実行制御

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

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

    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例外をキャッチするコードがあり、これが、コンカレンシーの例外処理です。

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

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

スクリーンショット 2019-11-04 13.12.38.png

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

モデルバインディング

OnGetAsync メソッド

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

OnGetAsync メソッドでは、最後に

return Page();

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

@model RazorPagesMovie.Pages.Movies.EditModel

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

OnPostAsyncメソッド

EditModelクラスには、

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

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

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

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

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

この後で、

await _context.SaveChangesAsync();

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

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?