記事目次
- ASP.NET Core 3.0 Razor Pages 事始め(1) - はじめてのRazor Pagesアプリケーション
- ASP.NET Core 3.0 Razor Pages 事始め(2) - スキャフォールディングとDBマイグレーション
- ASP.NET Core 3.0 Razor Pages 事始め(3) - マイグレーションのやり直しとURLルーティング
- ASP.NET Core 3.0 Razor Pages 事始め(4) - ページモデルとページハンドラ
- ASP.NET Core 3.0 Razor Pages 事始め(5) - Postページハンドラとタグヘルパー
- ASP.NET Core 3.0 Razor Pages 事始め(6) - データベースに初期値を設定する
- ASP.NET Core 3.0 Razor Pages 事始め(7) - Viewの変更とコンカレンシー例外処理 <-- この記事
- ASP.NET Core 3.0 Razor Pages 事始め(8) - 検索機能の追加
- ASP.NET Core 3.0 Razor Pages 事始め(9) - ページに新しいフィールドを追加する
- ASP.NET Core 3.0 Razor Pages 事始め(10) - 検証機能の追加
- 前回ASP.NET Core 3.0 Razor Pages 事始め(3)の続きです。
- 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 | Delete
と Create New
の部分も日本語に変更します。
では、テーブルのデータは全部消してから、再度アプリを起動し直します。
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-page
と asp-route-id
から生成されているのが分かります。
id=3
の 3
は、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
例外が発生します。
デバッグでブレークポイントを設定すると、それを確かめることができます。
ちなみに、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
になっています。