その他のアクションについても見てみる
改めてFootmarkコントローラについて、他のアクションも見ていきましょう。アクションメソッドには、Index(GET)、Details(GET)、Create(GET、POST)、Edit(GET、POST)、Delete(GET、POST)の8個があります。HTTP GETで呼び出されるメソッドは表示系、HTTP POSTメソッドで呼び出されるメソッドは更新系です。表示系のIndexアクションを見てきましたので、ここからは更新系のCreateアクションを中心に見てみましょう。
コントローラ(Createアクション) - Controllers/FootmarkController.csファイル
Controllers/FootmarkController.csファイルからCreateアクションのみを抜粋します(リスト3)。
// Createアクション(GET) public IActionResult Create() (1) { return View(); } // Createアクション(POST) [HttpPost] (2) [ValidateAntiForgeryToken] (3) public async Task<IActionResult> Create([Bind("ID,YourName,Message,VisitTime")] Footmark footmark) (4) { if (ModelState.IsValid) (5) { _context.Add(footmark); (6) await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(footmark); (7) } …略…
Createアクションのメソッドは、HTTP GETとPOSTで2つあります。このうちGETに対応するメソッドは、単にフォームを表示するだけなのでViewメソッドの呼び出しがあるのみです(1)。(4)はPOSTに対応する、すなわちフォームの送信で呼び出されるメソッドになります。Indexアクションと同様に非同期実行になります。短いコードですが、更新系のメソッドで重要な要素が多数盛り込まれています。
(2)と(3)はアクションメソッドの属性です。HttpPost属性は、アクションメソッドがHTTP POSTに対するものであることを指定します。この属性のないメソッドは、HTTP GETに対するものとなります(HttpGet属性は既定値なので通常は記述しません)。(3)は、クロスサイトリクエストフォージェリ(CSRF)攻撃に対応するためのValidateAntiForgeryToken属性です。HTTP POSTのためのメソッドでは、この2つの属性がセットと言えるでしょう。
後述しますが、method属性が"POST"であるフォームには自動的にCSRF攻撃対策用トークンが隠しフィールドとして埋め込まれ、フォームからそのまま送信されます。アプリケーション側は、フォームに埋め込んだトークンと送られてきたトークンを比較し、一致したときのみアクションを実行します(図4)。
再び(4)ですが、特徴的な構文となっています。引数Footmarkはモデルオブジェクトですが、Bind属性が付加されています。この属性は、メソッドが受け入れるモデルのプロパティを列挙したもので、それ以外のプロパティが送られてきてもモデルにバインドしないことを意味します(オーバーポスティング攻撃対策)。フォームによって更新されないフィールドなど、送信されるべきでないプロパティをバインドしない指定をすることで、改ざんされたPOSTデータによる攻撃を防ぐことができます(図5)。この場合はFootmarkモデルの全てのプロパティが列挙されているので、これらを全て受け入れます。
(5)は、フォームの送信で呼び出されるメソッドで必要な構文です。IsValidメソッドは、送信されてきたデータが検証(バリデーション)を通過したときにtrueになります。そうでないときはfalseになるので、そのまま(7)のようにモデルデータをViewメソッドの引数に与えて、自ら(フォーム)を再表示します。
バリデーションを通過したときには、(6)からの処理を順番に実行します。Addメソッドでモデルデータをデータベースコンテキストに書き込み、SaveChangesAsyncメソッドで非同期にデータベースに反映します。最後は、RedirectToActionメソッドでIndexアクションにリダイレクトします。なお、RedirectToActionメソッドの戻り値はRedirectToRouteResult型で、ActionResult型を継承するクラスです。
ビュー(Createアクション) - Views/Footmark/Create.cshtmlファイル
ビューのファイルはViews/Footmark/Create.cshtmlファイルです(リスト04)。このビューは、HTTP GETとPOSTの双方のCreateアクションで呼び出されます。
@model RazorPagesSample.Models.Footmark (1) …略… <div class="row"> <div class="col-md-4"> <!-- POSTメソッドでCreateアクションに送信するフォーム --> <form asp-action="Create"> (2) <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-group"> <!-- asp-forタグヘルパーにはフィールドのみを与える --> <label asp-for="YourName" class="control-label"></label> (3) <input asp-for="YourName" class="form-control" /> <span asp-validation-for="YourName" class="text-danger"></span> </div> …略… </form> </div> </div> …あとはRazor Pagesにおけるものと同じなので省略…
Createアクションのビューはフォームです。初期状態のフォーム、あるいはフォームの送信でバリデーションに問題があった場合の再表示に使用されます。
(1)は、Indexアクションのビューで触れたように、ビューに渡されるViewModelの型です。ここではFootmarkモデルが直接渡されています。なお、HTTP GETのCreateアクションではViewメソッドの引数は空でしたので、この場合ビューに渡されるViewModelはnullになります。
(2)はフォームの開始です。Razor Pagesではaction属性がなく、method属性で"POST"が指定されているのみで、自分自身にポストバックするという動きでした。ここでは、asp-actionタグヘルパーにてCreateアクションを呼び出します。このとき、自動的にmethod属性に"POST"が指定されるので、今度はHTTP POSTのCreateメソッドが実行され、データが登録されます。CSRF攻撃対策用トークンも送信されます。
(3)はフォーム要素とモデルの関連付けですが、Razor Pagesではページモデルが渡されているので、そのプロパティであるFootmarkを使ってフィールドを参照します。ここではFootmarkモデルそのものが渡されているので、フィールドを直接参照できます。なお、初期表示のフォームではViewModelはnullなので、asp-forヘルパーによる参照の結果はブランクになります。
コントローラ(その他) - Controllers/FootmarkController.csファイル
最後に、その他のアクションで特徴的な構文を見てみます(リスト5)。
// Editアクション(POST) public async Task<IActionResult> Edit(int id, [Bind("ID,YourName,Message,VisitTime")] Footmark footmark) { …略… try (1) { _context.Update(footmark); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) (1) { if (!FootmarkExists(footmark.ID)) { return NotFound(); } else { throw; } } …略… } …略… // Deleteアクション(POST) [HttpPost, ActionName("Delete")] (4) [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(int id) …略…
Editメソッドでは、Createメソッドにはなかったtry〜catch文があります(1)。これは、catchしている例外のDbUpdateConcurrencyExceptionから分かるように、同時にレコードを更新しようとした更新特有の状況に対処しています。例外のキャッチでレコードの存在をチェックし、見つからなければ更新できないとしてNotFound()メソッドの戻り値(ActionResult型を継承するNotFoundObjectResult型)を返します。見つかれば競合として例外をさらにスローします。
(4)はHttpPost属性ですが、引数ActionNameが指定されています。この引数は、アクションメソッドの名前が実際のアクションと異なる場合に指定するものです。2つあるDeleteメソッドは引数が同じ、すなわちシグネチャが同一ということになるので、片方の名前を変えてあげる必要があるのです。このようなときに引数ActionNameを指定してアクション名としては同一となるようにします。
まとめ
今回はASP.NET Core MVCのまとめとして、ScaffoldingによるCRUD機能の追加や、そのコンロトーラやビューの解説を行いました。作成されるソースコードはRazor Pagesに比べると若干複雑になりますが、基本的にシンプルな構成になっていることがお分かりいただけたのではないかと思います。次回は、同じくASP.NET CoreのサブフレームワークであるBlazorを紹介します。