ASP.NET Core 3.0 Razor Pages 事始め(6)の続きです。
今回は公式チュートリアルのASP.NET Core アプリで生成済みページを更新するに沿って進めていこうと思います。
ページの見た目を変更する
まずは、Indexページの見た目の変更です。
チュートリアルでは、表のヘッダーの表示と、価格の表示を変更していますが、せっかくなので日本語で表示させようと思います。
まずは、Models/Movie.csを開いて、各プロパティに属性を追加します。
publicclassMovie{publicintID{get;set;}[Display(Name="タイトル")]publicstringTitle{get;set;}[DataType(DataType.Date)][Display(Name="リリース日")]publicDateTimeReleaseDate{get;set;}[Display(Name="ジャンル")]publicstringGenre{get;set;}[Display(Name="価格")][DisplayFormat(DataFormatString="{0:#,0}")]publicdecimalPrice{get;set;}}
[Display]
属性は、項目の名前として表示する文字列を指定します。Indexページでは、表のヘッダー部にこの文字列が表示されます。
[DisplayFormat]
属性は、フォーマットの書式を指定します。チュートリアルでは、
[Column(TypeName="decimal(18, 2)")]publicdecimalPrice{get;set;}
のように属性を追加する例が載っていますが、ここでは価格が円であると考え、この属性は取りました。
それと、初期データも、小数点を取って、適当な値に直しておきました。
そういう意味では、decimal
ではなく、int
に変更したほうが良いのかもしれませんが、またマイグレーションやるのも面倒なので、このままにします。
ついでに、Edit | Details | Delete
と Create New
の部分も日本語に変更します。
では、テーブルのデータは全部消してから、再度アプリを起動し直します。
OKのようです。
Indexページだけではなく、Editページなどほかのページも項目のラベルが日本語表記に変わりました。
変更したIndex.cshtmlもいちおう以下に示しておきます。
@page
@model RazorPagesMovie.Pages.Movies.IndexModel
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1><p><aasp-page="Create">新規追加</a></p><tableclass="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><aasp-page="./Edit"asp-route-id="@item.ID">編集</a> |
<aasp-page="./Details"asp-route-id="@item.ID">詳細</a> |
<aasp-page="./Delete"asp-route-id="@item.ID">削除</a></td></tr>
}
</tbody></table>
それと、Chromeで動かすと、「このページを翻訳しますか」と聞いてくるのがうっとおしいので、_Layout.cshtml を以下のように変更しました。
<!DOCTYPE html><htmllang="ja"><head>…
アンカー タグヘルパー
前回も書いたと思うけど、再度、Index.cshtmlで使われているアンカー タグヘルパーを見てみます。
<aasp-page="./Edit"asp-route-id="@item.ID">編集</a> |
<aasp-page="./Details"asp-route-id="@item.ID">詳細</a> |
<aasp-page="./Delete"asp-route-id="@item.ID">削除</a>
このタグヘルパーは、以下のようなHTMLに変換されます。
<ahref="/Movies/Edit?id=3">編集</a> |
<ahref="/Movies/Details?id=3">詳細</a> |
<ahref="/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に変換されるようになります。
<ahref="/Movies/Edit/3">編集</a> |
<ahref="/Movies/Details/3">詳細</a> |
<ahref="/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が渡ってきます。
これで、プログラムコード側で、省略された場合の動作を指定できるようになります。まあ、このアプリの場合はNullableにする必要性はないですが、あくまでも実験ということで。
楽観的同時実行制御
チュートリアルでは、「コンカレンシーの例外処理」という用語を使っていますね。
Edit.cshtml.cs を確認してみます。データ更新のメソッド OnPostAsync
を抜きだしてみます。
publicasyncTask<IActionResult>OnPostAsync(){if(!ModelState.IsValid){returnPage();}_context.Attach(Movie).State=EntityState.Modified;try{await_context.SaveChangesAsync();}catch(DbUpdateConcurrencyException){if(!MovieExists(Movie.ID)){returnNotFound();}else{throw;}}returnRedirectToPage("./Index");}
DbUpdateConcurrencyException
例外をキャッチするコードがあり、これが、コンカレンシーの例外処理です。
試しに、ブラウザを2つ立ち上げて、同じMovieに対して、片方では編集ページ、もう片方では削除ページを開き、2つめのページで削除してから、編集ページでデータを更新すると、DbUpdateConcurrencyException
例外が発生します。
デバッグでブレークポイントを設定すると、それを確かめることができます。
ちなみに、2つのページで編集を開き、別の値で更新した時には、DbUpdateConcurrencyException
例外が発生しませんでした。ASP.NET MVCと同様、rowversionカラムが必要みたいです。SQLiteはサポートしているのかな?
モデルバインディング
OnGetAsync
メソッド
もう一度、Edit.cshtml.cs を見てみます。
OnGetAsync
メソッドでは、最後に
returnPage();
とすることで、Pages/Movies/Edit.cshtml Razor ページをレンダリングします。 Edit.cshtml ファイルでは
@model RazorPagesMovie.Pages.Movies.EditModel
の行があるので、EditModelオブジェクトが使用できるようになります。あとは、Razor構文を使って、モデルの値をHTMLとバインドしていきます。
OnPostAsync
メソッド
EditModel
クラスには、
[BindProperty]publicMovieMovie{get;set;}
というプロパティがあります。これが、ビューとバインドするデータになります。
この[BindProperty]
属性を付けることで、クライアントから送信されてきたデータが、Movieプロパティにバインドされるようになります。
OnPostAsync
メソッドの中に入ってきたときには、既に、Movieプロパティには値が設定されているので、このサンプルでは、これをそのまま
_context.Attach(Movie).State=EntityState.Modified;
とすることで、DbContextに、Movieオブジェクトをアタッチして、状態を変更済みにしています。
この後で、
await_context.SaveChangesAsync();
とすれば、DBが更新されます。
なお、モデルにサーバー側で検知された検証エラーがあれば、ModelState.IsValid
の値は、false
になっています。