連載:〜ScottGu氏のブログより〜

ASP.NET MVC Preview 5とフォーム送信シナリオ

Scott Guthrie 著/Chica
2008/9/12
Page1 Page2 Page3 Page4

モデル・バインダ(Model Binder)

 上記のサンプルでは、フォーム送信を処理するコントローラのアクション・メソッドのシグネチャは、メソッドの引数としてStringおよびDecimalを取ります。そしてアクション・メソッドは新しいProductオブジェクトを生成し、これらの入力値をそこへ割り当て、それをデータベースへ挿入しようとします(図8)。


図8

 このシナリオをよりクリーンにする“Preview 5”での新機能の1つに“モデル・バインダ”があります。モデル・バインダは、複雑な型に対して、HTTP入力値をデシリアライズしコントローラ・アクション・メソッドへそれを引数として引き渡す方法を提供します。また入力の例外処理も提供しているので、エラー発生時のフォームの再表示が簡単になります(エンドユーザーにすべてのデータを再入力してもらわなくても済みます。これについての詳細は後ほど)。

 例えば、モデル・バインダを使用すると、上記のアクション・メソッドをリファクタリングして、Productオブジェクトを次のように引数として受け取るようにできます(図9)。


図9

 これによりコードを少し簡易でクリーンにすることができます。また、複数のコントローラやアクションに散らばっている、フォームをパースするコードの繰り返しが必要なくなります(DRY(Don't Repeat Yourself)プリンシパルを守ることができます)。

■モデル・バインダの登録

 ASP.NET MVCにおけるモデル・バインダは、IModelBinderインターフェイスを実装するクラスで、入力パラメータの型のバインドを管理するために使用することができます。モデル・バインダは特定のオブジェクトの型に対して書くことも、広い範囲の型を処理するために使用することも可能です。このIModelBinderインターフェイスにより、Webサーバまたは特定のコントローラ実装とは独立して、バインダの単体テストを行うことができます。

 モデル・バインダは、ASP.NET MVCアプリケーションに関して、4つの異なるレベルで登録することができ、これにより非常に柔軟に活用できます。

1)ASP.NET MVCはまず、アクション・メソッドでパラメータ属性として宣言されているモデル・バインダがあるか検索します。例えば、仮に以下のような属性を使用して製品のパラメータに注釈を付け、“Bind”バインダの使用を示すことができます(属性でパラメータを使用して、2つのプロパティだけがバインドされる様子を確認してください)(図10)。


図10

注意:“Preview 5”は上記のようなビルドインの[Bind]属性はまだありません(今後ASP.NET MVCのビルドイン機能として追加することを現在考慮中ですが)。しかしながら、上記のような[Bind]属性を実装するために必要なフレームワーク構造のすべては、現在Preview5で実装されています。オープンソースのMVCContribプロジェクトには、現在使用できる上記のようなDataBind属性もあります。

2)もしアクション・パラメータにバインダ属性がない場合、ASP.NET MVCはアクション・メソッドに引き渡されるパラメータの型に、属性として登録されているバインダがあるかどうか検索します。例えば、Productの部分クラスに以下のようなコードを追加して、LINQ to SQL の“Product”オブジェクトに対して明示的に“ProductBinder”バインダを登録することができます(図11)。


図11

3)ASP.NET MVCには、ModelBinders.Bindersコレクションを使用して、アプリケーションのスタートアップでバインダを登録する機能もあります。これは(注釈できない)サード・パーティにより書かれた型を使用する場合や、モデル・オブジェクトに直接バインダ属性の注釈を追加したくない場合に便利です。以下のコードでは、global.asaxのアプリケーション・スタートアップで2つの型を特定したバインダを登録する方法を示しています(図12)。


図12

4)型を特定したグローバル・バインダの登録とは別に、ModelBinders.DefaultBinderプロパティを使用して、デフォルトのバインダを登録すると、型を特定したバインダが見つからない場合に使用されるようになります。(現在、MVCのPreviewビルドではデフォルトで参照されるようになっている)MVCFuturesアセンブリに含まれているのはComplexModelBinderの実装で、これはフォームから送信される名前や値に基づいてプロパティを設定するためにリフレクションを使用します。以下のコードを使用すれば、コントローラのアクション引数として引き渡されるすべての複雑な型に対するフォールバックとして使用されるように、ComplexModelBinderを登録することができます(図13)。


図13

注意:MVCチームは次回のドロップで、IModelBinderインターフェイスを変更する予定です(最近、変更が必要なシナリオがいくつか見つかったためです)。Preview 5でモデル・バインダをカスタマイズして構築する場合、次のドロップが公開されたとき、いくつか変更が必要になることをご了承ください(恐らくそれほど大きなものではないですが、そのメソッド上でいくつかの引数が変更されることが判明しているということの告知です)。

UpdateModelおよびTryUpdateModelメソッド

 上記のModelBinder機能は新しいオブジェクトを初期化して、それらをコントローラのアクション・メソッドへ引き渡したいようなケースで便利です。それ以外に、アクション・メソッド内で自分自身を検索・作成する既存のオブジェクトのインスタンスに入力値をバインドしたいケースもあります。例えば、データベースにある既存の製品に対して編集を可能にした場合では、アクション・メソッド内において、ORMを使用して既存製品のインスタンスをデータベースからまず検索し、その後、検索された製品のインスタンスへ新しい入力値をバインドし、データベースへそれらの変更を保存しようとするかもしれません。

 “Preview 5”では、これを可能にするために、コントローラの基本クラスで2つの新しいメソッドを追加しています。UpdateModel()とTryUpdateModel()です。両方とも、最初の引数として既存のオブジェクト・インスタンスを、2つ目の引数としては、フォーム送信された値を使用して更新したいプロパティのセキュリティ・ホワイトリストを引き渡すことが可能です。例えば以下では、LINQ to SQLを使用してProductオブジェクトを取得し、UpdateModelメソッドを使用して製品名および価格のプロパティをフォームのデータで更新しています(図14)。


図14

 UpdateModelメソッドは、リストにあるすべてのプロパティを更新しようとします(リストの最初の方でエラーがあった場合でも)。もし、あるプロパティに対してエラーが発生した場合(例えば、DecimalのUnitPriceプロパティに対してStringのデータが渡された場合など)、“Preview 5”で追加された新しい“ModelState”コレクションに、発生した例外オブジェクトとフォームから送信された元の値が保存されます。この新しいModelStateコレクションは後で取り上げますが、簡単にいうと、エラーが発生した後、ユーザーが修正した値とともに自動的にフォームを再表示する簡単な方法を提供します。

 すべての表示されたプロパティの更新を試行した後で、UpdateModelメソッドは失敗があった場合に例外を発生させます。TryUpdateModelメソッドは同じ方法で動作しますが、例外を発生させる代わりに、booleanでTrueまたはFalseの値を返して、エラーがあったかどうかを示します。エラー処理方法に応じて適切なものを選択できます。

■製品の編集例

 UpdateModelメソッドが使用されたときの例を見るために、簡単な製品の編集フォームを実装してみましょう。/Products/Edit/{ProductId}のようなURL形式を使用して、どの製品を編集するか示します。例えば、以下のURLは/Products/Edit/4です。これは、編集する製品のProductIdが4であるという意味になります(図15)。


図15

 ユーザーは製品名や価格を変更して、Saveボタンをクリックすることができます。ユーザーが送信のアクション・メソッドを実行すると、データベースを更新し、成功した場合“Product Updated!”メッセージが表示されます(図16)。


図16

 以下のように、2つのコントローラ・アクション・メソッドを使用して上記の機能を実装できます。[AcceptVerbs]属性を使用して、初期フォームを表示するEditアクションと、フォーム送信を処理するEditアクションを区別しているのを確認してください(図17)。


図17

 上記のPOSTアクション・メソッドは、編集するProductオブジェクトのインスタンスをLINQ to SQLを使用してデータベースから取得し、UpdateModelを使用して製品のProductNameおよびUnitPriceの値を、フォームから送信された値に更新します。その後、LINQ to SQLのデータ・コンテキスト上でSubmitChanges()を呼び出してデータベースに更新部分を保存します。もしそれが成功すれば、成功のメッセージ文字列をTempDataコレクションに保存し、クライアント・サイドのリダイレクトを使用してGETアクション・メソッドへユーザーをリダイレクトします(これにより、更新されたことを示すTempDataメッセージ文字列とともに、新しく保存された製品が再表示されるようになります)。もしフォームで送信された値もしくは更新されたデータベースでエラーが発生した場合、例外が発生してCatchブロックで捕捉され、ユーザーがそれらを修正できるように、そのフォームを再表示します。

 成功した場合のこのリダイレクトが何なのか、なぜ単純にフォームを再表示して成功したメッセージを表示しないのか、と不思議に思われているかもしれません。クライアントのリダイレクトを行う理由は、ユーザーが保存ボタンを押して成功した後に更新ボタンを押した場合、そのフォームを再送信しないようにし、次のようなブラウザ・メッセージが表示されるようにするためです(図18)。


図18

 アクション・メソッドのGETバージョンへリダイレクトする場合では、ユーザーが更新ボタンを押したときに単純にページをリロードするだけで、ポストバックはしないようにします。この方法は“Post/Redirect/Get”(PRG)パターンと呼ばれています。Tim BarczがASP.NET MVCにおけるこの詳細についてここで語っています。

 上記の2つのコントローラのアクション・メソッドは、すべてProductオブジェクトの編集および更新を処理するために実装する必要があります。以下は、上記のコントローラで表示される“Edit”ビューです(図19)。


図19

便利なTIP:以前は、URL(例:/Products/Edit/4)にパラメータを追加する場合、送信するURLにパラメータを含めるためにフォームのアクション属性を更新するコードをビューに書かなければなりませんでした。“Preview 5”では、Html.Form()ヘルパー・メソッドがあるため、これが簡単になっています。Html.Form()はオーバーロードされたバージョンが多数あり、指定できるパラメータのオプションも数多くあります。新しくオーバーロードされた、パラメータを取らないHtml.Form()メソッドも追加されており、現在のリクエストと同じURLを出力します。

 例えば、上記のビューを描画するコントローラに対するURLが“/Products/Edit/5”だった場合、上記のようにHtml.Form()を呼び出して自動的に、

<form action="/Products/Edit/5" method="post">

を出力タグとして出力します。もし上記のビューを描画するコントローラに対するURLが“/Products/Edit/55”だった場合、上記のようにHtml.Form()を呼び出して、自動的に、

<form action="/Products/Edit/55" method="post">

を出力タグとして出力します。これにより、URLの構築やパラメータの表示を行うために、コードを書く必要がなくなります。

■単体テストとUpdateModel

 今回公開されたPreview 5では、UpdateModelメソッドは値を取得するために、リクエスト・オブジェクトのフォーム送信コレクションに対して常に動作しています。つまり、上記のフォーム送信のアクション・メソッドをテストする場合、単体テストでリクエスト・オブジェクトをモックする必要はありません。

 次回公開のMVCでは、オーバーロードされたUpdateModelメソッドが追加され、代わりとなる値の独自のコレクションが引き渡せるようになります。例えば、新しいFormCollection型がPreview5で使用でき(ModelBuilderがあり、自動的にすべてのフォーム送信された値がひも付けられます)、次のように引数としてUpdateModelメソッドに引き渡すことができます(図20)。


図20

 上記のような方法を使用すると、フォーム送信のシナリオをモックを使わずに単体テストを行えます。以下は単体テストの例で、POSTの場合に新しい値で更新し、リダイレクトによりアクション・メソッドのGET版へ戻るというテストを書くことができます。すべての機能をコントローラで単体テストするために、どんなモックも(また特別なヘルパー・メソッドに頼ることも)必要ありません。


図21


 INDEX
  〜ScottGu氏のブログより〜
  ASP.NET MVC Preview 5とフォーム送信シナリオ
    1.Web MVCパターンによる基本的なフォーム送信(Form Post)
  2.モデル・バインダ(Model Binder)/UpdateModelおよびTryUpdateModelメソッド
    3.エラー処理のシナリオ ―― フォームの再表示でエラー・メッセージを表示
    4.LINQ to SQL Entityへのビジネス・ルールの追加
 
インデックス・ページヘ  「〜ScottGu氏のブログより〜」


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 記事ランキング

本日 月間