この記事はASP.NET Advent Calendar 2014の23日目となる記事です。
#モデルバインディングとは
クライアントから送信されてきたデータを、コントローラのアクションメソッドの引数にバインドするための処理のことです
※Web FormsでもASP.NET 4.5からモデルバインディングが使用可能なため[アクションメソッド]と呼ぶのは語弊があるかもしれませんが、サーバーサイドの処理の引数に対して値を設定する処理という意味で読んで頂ければと思います
自分自身きちんと理解できていない部分もあったので、調べてみました。
※MVCのモデルバインディングの概要は、以下にも記載しております
ASP.NET MVC 開発を始める前に理解しておきたいこと モデルバインディング
#MVC モデルバインディング
ASP.NET MVC 5 APPLICATION LIFECYCLE
この資料の真ん中あたりにModel Bindingがあります
認証、認可が終わった後、Actionメソッドを呼び出す前に実行されます
System.Web.Mvc 名前空間配下にある[ValueProvider]、[ModelBinder]といった名前がついているクラスを使用します
MVCは、他のもののようにModelBinder用の名前空間は作成されていないようです
関係としては、ModelBinderがValuProviderから必要なデータを取得し、Modelに対して値の設定を行うようになります
##ValueProvider(値の取得先)
クエリ文字列、フォーム、RouteData(ルーティング時に設定される項目)、といったリソースからデータを取得できるようにします
System.Web.Mvc配下に、代表的なものとして以下のValueProviderが実装されています
クラス名 | 概要 |
---|---|
ChildActionValueProvider | 子アクションからの値取得 |
FormValueProvider | フォーム値からの値取得 |
HttpFileCollectionValueProvider | HTTP ファイルのコレクションから取得 |
QueryStringValueProvider | クエリ文字列からの値取得 |
RouteDataValueProvider | ルートデータからの値取得 |
ValueProviderを実装するために使用されるIFには以下のようなものがあります
クラス名 | 概要 |
---|---|
IValueProvider | ASP.NET MVC の値プロバイダーに必要なメソッドを定義します。なのでValueProviderとして使うためには、基本的にはこれを実装します |
IEnumerableValueProvider | 列挙可能にする機能を持つ特殊な IValueProvider |
IUnvalidatedValueProvider | 要求の検証をスキップできる IValueProvider |
デフォルトでは、以下の順番でValueProviderが登録されています
ModelBinderは、ValueProviderに登録されている順番に必要なデータが存在するか検証します
登録順での検索となるため同じキー名で、Formと、クエリ文字列に値が存在した場合は、Formが優先されるようになります
##ModelBinder(モデルの作成)
ValueProviderを使って、引数の名前と一致するものを探して、値を設定していきます
基本的には、DefaultModelBinder クラスでほとんど処理が可能です
対応している型が以下のためです
- String 、Double、Decimal、DateTime オブジェクトなどのプリミティブ型
- Person、Address、Product などのモデル クラス
- ICollection<T> 、IList<T>、IDictionary<TKey, TValue> などのコレクション
DefaultModelBinder はブラウザー要求を、データオブジェクト(Actionの引数)に対応付けますが、以下のような流れで動作します
ASP.NET MVC モデル バインディングの特長と問題点
既定のモデル バインダー
- 値プロバイダーを調べて、プロパティ名がプレフィックスとして登録されているかどうかを確かめることで、そのプロパティが単純型と複合型のどちらの型として検出されたかを確認する。プレフィックスは、値が複合オブジェクトのプロパティかどうかを示すのに使用する、"ドット表記" の HTML フォーム フィールド名です。このプレフィックスのパターンは、[親プロパティ].[プロパティ] になります。たとえば、UnitPrice.Amount という名前のフォーム フィールドには、UnitPrice プロパティの Amount フィールドの値が含まれます
- プロパティ名を取得するため、登録済みの値プロバイダーから ValueProviderResult を受け取る
- 値が単純型の場合は、ターゲット型への変換を試みる。既定の変換ロジックでは、プロパティの TypeConverter を利用して、文字列型のソース値がターゲット型に変換されます
- それ以外の場合は、プロパティが複合型であるため、再帰的にバインドを行う
[1]の記述を少し補足すると、クライアントから送信されてきたデータのキー値に[.]が含まれる場合は、複合型と判断するような感じです
DefaultModelBinder 以外にも、HttpPostedFileBase(ファイル送信)、byte[]用のモデルバインダーが用意されています
##実際の使用例
普通の型で受ける場合はそこまで苦労しませんが、配列、Dictionaryがちょっと特殊なため、記載しておきます
参考URL
ASP.NET Wire Format for Model Binding to Arrays, Lists, Collections, Dictionaries
###プリミティブ型の配列
同じnameで送信する
public ActionResult Test(string[] values)
{
return RedirectToAction("Index");
}
@using (Html.BeginForm("Test", "Home"))
{
<input type="text" name="values" value="1"/>
<input type="text" name="values" value="2"/>
<input type="submit" name="Send" value="Send"/>
}
// クエリ文字列の場合も同じキー名で送信すればよい
http://localhost/Home/Test?values=1&values=2
###複合型の配列
[0]という形式でインデックス用の添え字を付ける
public ActionResult Test4(Employee[] employees)
{
return RedirectToAction("Index");
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
@using (Html.BeginForm("Test4", "Home"))
{
<input type="text" name="employees[0].Id" value="1" />
<input type="text" name="employees[0].Name" value="Bob" />
<input type="text" name="employees[1].Id" value="2" />
<input type="text" name="employees[1].Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
@* 以下のようにパラメータ名がなくても送信可能 *@
@using (Html.BeginForm("Test4", "Home"))
{
<input type="text" name="[0].Id" value="1" />
<input type="text" name="[0].Name" value="Bob" />
<input type="text" name="[1].Id" value="2" />
<input type="text" name="[1].Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
// クエリ文字列の場合もFormの時と同様
http://localhost/Home/Test4?[0].Id=1&[0].Name=Bob&[1].Id=2&[1].Name=Joe
http://localhost/Home/Test4?employees[0].Id=1&employees[0].Name=Bob&employees[1].Id=2&employees[1].Name=Joe
※Jsonで送信する場合も上記の命名規則に従う必要があります
// バインド不可
// Json的にはこっちの記載のほうが正しいですが、ModelBinderの動作となるので、それに合わせないと値が反映されない
[
{ "Id": 1, "Name": "Bob"},
{ "Id": 2, "Name": "Joe"},
]
// バインド可能
{
"employees[0].Id": 1,
"employees[0].Name": "Bob",
"employees[1].Id": 2,
"employees[1].Name": "Joe",
}
###Dictionary
複合型の配列と同様に[0]による添え字と、Key、Valueというプロパティに対してそれぞれ値を設定していく
public ActionResult Test5(Dictionary<int, Employee> values)
{
return RedirectToAction("Index");
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
@using (Html.BeginForm("Test5", "Home"))
{
<input type="text" name="[0].Key" value="1" />
<input type="text" name="[0].Value.Id" value="1" />
<input type="text" name="[0].Value.Name" value="Bob" />
<input type="text" name="[1].Key" value="2" />
<input type="text" name="[1].Value.Id" value="2" />
<input type="text" name="[1].Value.Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
@* 以下のようにパラメータ名がなくても送信可能 *@
@using (Html.BeginForm("Test5", "Home"))
{
<input type="text" name="[0].Key" value="1" />
<input type="text" name="[0].Value.Id" value="1" />
<input type="text" name="[0].Value.Name" value="Bob" />
<input type="text" name="[1].Key" value="2" />
<input type="text" name="[1].Value.Id" value="2" />
<input type="text" name="[1].Value.Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
// クエリ文字列の場合もFormの時と同様
http://localhost/Home/Test5?[0].Key=1&[0].Value.Id=1&[0].Value.Name=Bob&[1].Key=2&[1].Value.Id=2&[1].Value.Name=Joe
#Web APIのモデルバインディング
ASP.NET Web API HTTP Message Lifecycle
ASP.NET Web API HTTP メッセージ ライフサイクル 日本語版
この資料の左下あたりにModel Bindingがあります
MVCと同様に、認証、認可が終わった後、Actionメソッドを呼び出す前に実行されます
##URI、リクエストボディ、ヘッダ (値の取得先)
MVCでは、ModelBinderがValueProvider越しにリクエストデータを受け取っていましたが、Web APIでは、リクエストのデータソースに応じて直接値を取得するような処理となります
デフォルトでは、複合型はBodyから、プリミティブ型はURIから値を取得するようになっています
この取得先を変更したい場合、引数の前に属性を付けることで制御が可能です
public Employee Get([FromUri]int id, [FromBody]Employee employee)
{
return new Employee
{
Id = 1,
Name = "Bob"
};
}
FromUriAttribute クラス
URIから値を取得するように指定する
FromBodyAttribute クラス
リクエストボディから値を取得するように指定
ただし、一つの引数にしか使用することができない
そのため複数の複合型のデータをボディから取得するには、1つのオブジェクトにまとめて取得する必要がある
※このあたりは別途記載したい。取り急ぎは、以下を参照
Passing multiple POST parameters to Web API Controller Methods
Web API Signatures with Multiple Complex Parameters
HttpParameterBindingを実装することで、任意の部分のデータを読み込む事が出来るようになります
アクションメソッドの引数に、ヘッダ部から取得した値を設定したい場合にはこのあたりの対応が必要となります
##MediaTypeFormatter、ModelBinder、ValueProvider(モデルの作成)
MVCとは異なり、ModelBinder派生のクラスがモデルを作成するというよりは、それぞれの処理にてモデルを作成するようになります
※このあたり別途追記
##実際の使用例
MVCの指定方法とほぼ同じですが、Dictionaryにパラメータ名を指定するとバインド出来なくなるので、注意してください
###Dictionary
複合型の配列と同様に[0]による添え字と、Key、Valueというプロパティに対してそれぞれ値を設定していく
public Employee Post(Dictionary<int, Employee> values)
{
return new Employee
{
Id = 1,
Name = "Bob"
};
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
@* 以下のようにパラメータ名が設定されている場合、バインドできない *@
@using (Html.BeginForm("Post", "api/HomeApi"))
{
<input type="text" name="values[0].Key" value="1" />
<input type="text" name="values[0].Value.Id" value="1" />
<input type="text" name="values[0].Value.Name" value="Bob" />
<input type="text" name="values[1].Key" value="2" />
<input type="text" name="values[1].Value.Id" value="2" />
<input type="text" name="values[1].Value.Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
@* 以下のようにパラメータ名がない場合、バインドがうまくいく *@
@using (Html.BeginForm("Post", "api/HomeApi"))
{
<input type="text" name="[0].Key" value="1" />
<input type="text" name="[0].Value.Id" value="1" />
<input type="text" name="[0].Value.Name" value="Bob" />
<input type="text" name="[1].Key" value="2" />
<input type="text" name="[1].Value.Id" value="2" />
<input type="text" name="[1].Value.Name" value="Joe" />
<input type="submit" name="Send" value="Send" />
}
※記述足りない部分が多いので、別途追記いたします。。
明日は、kiyokuraさんです。