連載:ASP.NET MVC入門【バージョン3対応】

第3回 モデル・バインドとアノテーション検証の実装

山田 祥寛(http://www.wings.msn.to/
2011/05/20
Page1 Page2 Page3

 前回は、Entity Framework 4.1のコード・ファーストとASP.NET MVC 3のスキャフォールディング機能を利用して、簡単な書籍情報管理アプリケーションを作成した。

 今回は、前回作成したアプリケーションの中でも、新規の書籍登録フォームにフォーカスして、ASP.NET MVCによるデータ登録の基本について理解していく。また、本稿後半では、登録フォームの実装に欠かせない検証機能についても解説する。


図1 スキャフォールディング機能で自動生成された新規登録フォーム(検証機能を付加したもの)

データ登録の基本

 それではさっそく、具体的なコードを見ていこう。

■Createアクション・メソッド

 まずは、コントローラのコードから。登録フォームの表示に関係するのは、入力フォームを生成するためのCreate()メソッドと、フォームから入力された値を処理するためのCreate(book)メソッドである。

// 入力フォームを生成するためのCreateアクション
public ActionResult Create()
{
  return View();
}

// [Create]ボタンをクリックしたときに呼び出されるCreateアクション
[HttpPost]
public ActionResult Create(Book book)
{
  if (ModelState.IsValid)
  {
    db.Books.Add(book);
    db.SaveChanges();
    return RedirectToAction("Index");
  }
  return View(book);
}
' 入力フォームを生成するためのCreateアクション
Function Create As ViewResult
  return View()
End Function

' [Create]ボタンをクリックしたときに呼び出されるCreateアクション
<HttpPost()>
Function Create(book As Book) As ActionResult
  If ModelState.IsValid Then
    db.Books.Add(book)
    db.SaveChanges()
    Return RedirectToAction("Index")
  End If
  Return View(book)
End Function
リスト1 登録フォームを生成するためのCreateアクション(上:BooksController.cs、下:BooksController.vb)

 Create()アクションについては、特筆すべき点はない。行うべき処理がない場合、アクション・メソッドには、ただActionResultオブジェクトを返すためのコード(ここではViewメソッド)を記述すればよいだけだ。

 リスト1で注目してほしいのは、ポスト・データが送信されたときに呼び出されるCreate(book)メソッドである。なお、その中でも薄字の部分は、入力値検証で利用するコードであり、現時点ではまだ気にする必要はない。

HTTPメソッドで処理を分岐するHttpPost属性

 アクション・メソッドがHTTP GET/HTTP POSTのいずれを経由して呼び出されたかによって処理を分岐したい、というケースはよくあることだ。例えば、リスト1の例であれば、以下のように書くことでCreate(book)メソッドを、Create()メソッドと1つにまとめることもできる。

if (Request.HttpMethod == "GET")
{
  // HTTP GET経由で呼び出された場合の処理
}
else
{
  // HTTP POST経由で呼び出された場合の処理
}
リスト2 Create()メソッドでGETとPOSTの両方を扱う場合

 しかし、このような書き方は、単体テストの行いやすさという点を1つとっても、あまり望ましくない。Requestオブジェクト依存のコードを記述するということは、単体テストに際して、Requestオブジェクトのモック(テストのためのダミーのオブジェクト)を用意しなければならないということを意味するからだ。

 では、別の方法として、フォームを表示するためのBooks/Newアクション、入力値を処理するためのBooks/Createアクションのように、別々のアクションを用意したらどうだろう。この場合、われわれはまた別な問題に遭遇することになる。

 例えば、エンド・ユーザーがBooks/Createアクションに直接アクセスしてしまったとしたらどうだろう。Books/Createアクションはフォーム経由で呼び出されることを想定しているアクションであるから、これは予期せぬエラーの原因になるかもしれない(そうでなくても、意図した結果は得られないだろう)。

 また、Books/Createアクションを検索エンジンのクローラが収集してしまうかもしれない。いうまでもなく、Books/Createはクローラによって収集されてよいURIではないので、このようなURIが公開されていることは、SEO(Search Engine Optimization)という観点からも好ましい状態ではない。

 そこで登場するのがHttpPost属性だ。HttpPost属性を利用することで、そのアクション・メソッドがHTTP POST経由でのリクエストによってのみ呼び出されることを宣言できる*1。リスト1の例であれば、Create(book)メソッドにHttpPost属性が付与されているので、HTTP POST経由でBooks/Createが呼び出された場合にのみCreate(book)メソッドが、それ以外のHTTPメソッド(普通はHTTP GET)でBooks/Createが呼び出された場合にはCreate()メソッドが呼び出されることになる。

*1 HttpPostはASP.NET MVC 2.0から導入された属性だ。ASP.NET MVC 1.0では、代わりに[AcceptVerbs(HttpVerbs.Post)] 属性を利用する必要がある。

入力値をオブジェクトにバインドする

 アクション・メソッドでは、モデル・オブジェクト(エンティティ)を引数として受け取るようにしておくだけで、入力値をオブジェクトのプロパティに自動的に割り当てることが可能だ(これを「モデル・バインド機能」と呼ぶ)。

 今回の場合、引数としてBookオブジェクトが指定されているので、Bookオブジェクトのisbnプロパティには入力要素isbnの値が、titleプロパティには入力要素titleの値が、それぞれ順に割り当てられるわけだ。

 リスト1では、入力値がバインドされたBookオブジェクトbookを、Books.Addメソッドでコンテキスト・オブジェクトに追加したうえで、SaveChangesメソッドによってデータベースに反映している。エンティティ経由でのデータ更新についてはEntity Frameworkに属する話であるので、詳細は別稿「Entity Frameworkにおけるクエリと更新」も併せて参照いただきたい。

処理後に別のアクションを呼び出す

 アクションを処理した後でビュー・スクリプトを呼び出す代わりに、別のアクションを呼び出すことも可能だ。これには、Viewメソッドの代わりに、RedirectToActionメソッドを呼び出せばよい。

 RedirectToActionメソッドの構文は以下のとおり。

RedirectToAction (string action
                    [,string controller] [,object routeValues])
RedirectToActionメソッドの構文
・action:アクション名
・controller:コントローラ名
・routeValues:ルーティング・ルールに引き渡すパラメータ

 引数controllerが省略された場合には、同一クラスの中から指定されたアクションが検索され、処理をリダイレクトする。引数routeValuesについては、本連載後半でルーティングの回として解説する予定だ。

■ビュー・スクリプトCreate.cshtml/Create.vbhtml

 続いては、Create()アクションに対応するビュー・スクリプトCreate.cshtml/Create.vbhtmlだ。なお、以下リストの薄字は、入力値検証で利用するコードであり、現時点では取りあえず無視して構わない。

@model MvcApp.Models.Book

@{
  ViewBag.Title = "Create";
}

<h2>Create</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
  @Html.ValidationSummary(true)
  <fieldset>
    <legend>Book</legend>

    <div class="editor-label">
      @Html.LabelFor(model => model.Isbn)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Isbn)
      @Html.ValidationMessageFor(model => model.Isbn)
    </div>

    <div class="editor-label">
      @Html.LabelFor(model => model.Title)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Title)
      @Html.ValidationMessageFor(model => model.Title)
    </div>

    <div class="editor-label">
      @Html.LabelFor(model => model.Price)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Price)
      @Html.ValidationMessageFor(model => model.Price)
    </div>

    <div class="editor-label">
      @Html.LabelFor(model => model.Publish)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Publish)
      @Html.ValidationMessageFor(model => model.Publish)
    </div>

    <div class="editor-label">
      @Html.LabelFor(model => model.Published)
    </div>
    <div class="editor-field">
      @Html.EditorFor(model => model.Published)
      @Html.ValidationMessageFor(model => model.Published)
    </div>

    <p>
      <input type="submit" value="Create" />
    </p>
  </fieldset>
}

<div>
  @Html.ActionLink("Back to List", "Index")
</div>
@ModelType MvcAppVb.Book

@Code
  ViewData("Title") = "Create"
End Code

<h2>Create</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@Using Html.BeginForm()
  @Html.ValidationSummary(True)
  @<fieldset>
    <legend>Book</legend>

    <div class="editor-label">
      @Html.LabelFor(Function(model) model.Isbn)
    </div>
    <div class="editor-field">
      @Html.EditorFor(Function(model) model.Isbn)
      @Html.ValidationMessageFor(Function(model) model.Isbn)
    </div>
    <div class="editor-label">
      @Html.LabelFor(Function(model) model.Title)
    </div>
    <div class="editor-field">
      @Html.EditorFor(Function(model) model.Title)
      @Html.ValidationMessageFor(Function(model) model.Title)
    </div>

    <div class="editor-label">
      @Html.LabelFor(Function(model) model.Price)
    </div>
    <div class="editor-field">
      @Html.EditorFor(Function(model) model.Price)
      @Html.ValidationMessageFor(Function(model) model.Price)
    </div>

    <div class="editor-label">
      @Html.LabelFor(Function(model) model.Publish)
    </div>
    <div class="editor-field">
      @Html.EditorFor(Function(model) model.Publish)
      @Html.ValidationMessageFor(Function(model) model.Publish)
    </div>

    <div class="editor-label">
      @Html.LabelFor(Function(model) model.Published)
    </div>
    <div class="editor-field">
      @Html.EditorFor(Function(model) model.Published)
      @Html.ValidationMessageFor(Function(model) model.Published)
    </div>

    <p>
      <input type="submit" value="Create" />
    </p>
  </fieldset>
End Using

<div>
  @Html.ActionLink("Back to List", "Index")
</div>
リスト3 書籍情報の入力フォームを表示するためのビュー・スクリプト(上:Create.cshtml、下:Create.vbhtml)

 ここで注目しておきたいポイントは、以下のとおりだ。

主キー項目は自動生成されない

 実は、スキャフォールディング機能により自動生成されたCreate.cshtml/Create.vbhtmlは、そのままでは動作しないので要注意だ。というのも、スキャフォールディング機能では主キー列(ここではisbn列)が入力要素として展開されないためだ*2。リスト3の太字部分のように、手動で入力要素を追加しておこう。

*2 標準的なint型のId、<クラス名>Id列を利用している場合、主キーは自動連番となり、手入力は不要であるので、こうした問題は起こらない。

フォームの定義を効率化するビュー・ヘルパー

 これまでも何度か述べてきたように、ASP.NET MVCではビュー・スクリプトの開発を効率化するために、さまざまなビュー・ヘルパーを用意している。例えばリスト3でも以下のようなビュー・ヘルパーを利用している。

  • Html.BeginForm
  • Html.LabelFor
  • Html.EditorFor

 BeginFormメソッドは、using命令でもって宣言することで、usingブロックがそのまま<form>〜</form>タグで置き換えられる仕組みだ。引数を省略した場合には、<form>タグのaction属性には現在のアクションがそのままセットされる。つまり、この例では以下のような<form>タグが生成されるはずだ。

<form action="/Hello/Post/" method="post">

 もしもaction属性やmethod属性の値を変更したい場合には、BeginFormメソッドの引数を追加すればよい。例えば、以下はaction属性を「/Home/Process」、method属性を「get」にしたい場合のBeginFormメソッドの記述である(C#の例)。

@using (Html.BeginForm("Process", "Home", FormMethod.Get)) {

 LabelFor/EditorForメソッドは、渡されたプロパティに応じて、それぞれラベル(<label>要素)と入力要素を出力するためのメソッドだ。出力される入力要素は、プロパティの型に応じて決められる。例えば、

@Html.LabelFor(model => model.Isbn)
@Html.EditorFor(model => model.Isbn)

で、BookエンティティにおけるIsbnプロパティに結び付いたラベルと入力要素(テキストボックス)を表示せよ、という意味になる(modelは、@modelディレクティブで指定されたエンティティ)。

 

 INDEX
  ASP.NET MVC入門【バージョン3対応】
  第3回 モデル・バインドとアノテーション検証の実装
  1.データ登録の基本/Createアクション・メソッド/ビュー・スクリプト
    2.検証機能の実装/エンティティに検証ルールを追加
    3.ビュー・スクリプトでエラー・メッセージを表示
 
インデックス・ページヘ  「ASP.NET MVC入門【バージョン3対応】」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間