日々の開発でぶち当たった疑問解消のプロセスを残していきます。

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。
-------- : スポンサー広告 :
Pagetop

[ASP.NET MVC3] フォームタグを使用したモデルバインディング

モデルバインディングの機構

ASP.NET MVC3ではユーザが入力した値をアクションメソッドが簡単に受け取ることができるように、モデルバインディングと呼ばれる機構がアクションメソッドの呼出し前に動作します。 モデルバインディングを実行するのはSystem.Web.Mvc.IModelBinderの実装クラスで、既定ではSystem.Web.Mvc.DefaultModelBinderクラスが適用されます。 DefaultModelBinderクラスは内部で登録されているSystem.Web.Mvc.IValueProviderを使用して、実際にモデルバインディングを実行していきます。 IValueProviderの既定実装は以下の4つです。 (MVC3からChildActionValueProviderもあるけどあまり公の資料には存在しないので放置)

データの取得元 使用するIValueProviderの実装型
Request.Form System.Web.Mvc.FormValueProvider
RouteData.Values System.Web.Mvc.FormValueProvider
Request.QueryString System.Web.Mvc.QueryStringValueProvider
Request.Files System.Web.Mvc.HttpFileCollectionValueProvider

DefaultModelBinderは、これらのIValueProviderを使ってモデルバインディングを実行しています。

モデルバインディングの実装例(フォーム)

Request.Formとのモデルバインディングの例を挙げていきます。 Request.Formから取得できる値は、WebブラウザからPOSTされたフォームのデータです。 Request.Formのキーはinputタグのname属性値が使用されます。 以下に簡単な例を示します。 他のIValueProviderを使用する例はまたの機会に。。。

ビュー(HTMLタグで記述する場合)

@using (Html.BeginForm("Test", "Home", FormMethod.Post))
{
    <div>
        <input type="text" name="sample" />
    </div>
    <div>
        <input type="submit" value="テスト" />
    </div>
    <div>
        @ViewBag.Message
    </div>
}
    

コントローラー

public class HomeController : Controller
{
    [HttpPost]
    public ActionResult Test(string sample)
    {
        this.ViewBag.Message = sample;
        return this.View();
    }
}
    

ボタンを押下すると、ビューのフォームにあるname属性がsampleであるテキストボックスに入力した値がアクションメソッドの第1引数に渡されます。 name属性の値と、同じ名前のアクションメソッドの仮引数に入力された値をバインドしてくれます。

HtmlHelperを使うと、ビューにHTMLのinputタグを書かなくても良くなります。 同じアクションメソッドを使う場合、ビューは以下のように書くこともできます。

ビュー(HtmlHelperを使用する場合)

@using (Html.BeginForm("Test", "Home", FormMethod.Post))
{
    <div>
        @Html.Editor("sample")
    </div>
    <div>
        <input type="submit" value="テスト" />
    </div>
    <div>
        @ViewBag.Message
    </div>
}
    

どちらの場合もビューのテキストボックスに入力した値をアクションメソッドで取得できていることが確認できます。

次に、モデルクラスを作成してバインドする例を示します。 同じように入力された値をモデルクラスのオブジェクトにバインドしてみます。

モデルクラス

namespace MVC3Sample
{
    public class TestModel
    {
        public string Sample { get; set; }
    }
}
    

ビュー(HtmlHelperとモデルクラスを使用する場合)

@model MVC3Sample.TestModel
@using (Html.BeginForm("Test", "Home", FormMethod.Post))
{
    <div>
        @Html.EditorFor(model => model.Sample)
    </div>
    <div>
        <input type="submit" value="テスト" />
    </div>
    <div>
        @ViewBag.Message
    </div>
}
    

コントローラー

public class HomeController : Controller
{
    [HttpPost]
    public ActionResult Test(TestModel model)
    {
        this.ViewBag.Message = model.Sample;
        return this.View(model);
    }
}
    

上記の例ではアクションメソッドにTestModelオブジェクトが引き渡され、そのSampleプロパティにビューに入力した値が設定されます。

フォームタグのモデルバインディングにはまだまだ他の機能があります。 配列やリストにもバインドできます。 ディクショナリにバインドすることもできます。 このあたりも別稿にできたら、と言うことで後回しにしてしまいます。

落とし穴

さて、これで一見どのような方法でも同じようにモデルバインドできているように見えます。 しかし実際には大きな落とし穴があるのです。 それを確認するために以下のモデル、ビュー、コントローラーを実装してみます。

モデル

namespace MVC3Sample
{
    public class TestModel
    {
        public string TestInput1 { get; set; }
        public string TestInput2 { get; set; }
    }
}
    

ビュー

@model MVC3Sample.TestModel
@using (Html.BeginForm("Test", "Home", FormMethod.Post))
{
    <div>
        @Html.EditorFor(model => model.TestInput1)
    </div>
    <div>
        <input type="text" name="TestInput2" />
    </div>
    <div>
        @Html.Editor("TestInput3")
    </div>
    <div>
        <input type="text" name="TestInput4" />
    </div>
    <div>
        <input type="submit" value="テスト" />
    </div>
    <div>
        <p>@this.ViewBag.Message1</p>
        <p>@this.ViewBag.Message2</p>
        <p>@this.ViewBag.Message3</p>
        <p>@this.ViewBag.Message4</p>
    </div>
}
    

コントローラー

[HttpPost]
public ActionResult Test(
    TestModel model,
    string TestInput3,
    string TestInput4)
{
    ViewBag.Message1 =
        GetMessage(
            "HtmlHelperからカスタムクラスへのバインド",
            model.TestInput1);
    ViewBag.Message2 =
        GetMessage(
            "素のHTMLからカスタムクラスへのバインド",
            model.TestInput2);
    ViewBag.Message3 =
        GetMessage(
            "HtmlHelperから文字列へのバインド",
            TestInput3);
    ViewBag.Message4 =
        GetMessage(
            "素のHTMLから文字列へのバインド",
            TestInput4);
    return this.View(model);
}

private static string GetMessage(
    string name,
    string value)
{
    if (value == null)
    {
        return name + "の値は null でした。";
    }
    else if (string.IsNullOrEmpty(value))
    {
        return name + "の値は 空文字 でした。";
    }
    else
    {
        return name + "の値は " + value + " でした。";
    }
}
    

各テキストボックスに適当に値を入力して実行すると、正しくモデルバインドされる様子を確認できます。 しかし、すべてのテキストボックスを未入力状態にしてボタンを押下してみてください。 なぜか上2つのバインド結果はnull参照になってしまいます。

null 参照になってしまう例

この動作はDefaultModelBinderの仕様によって決まっているように見えます。 あまり深追いはしていないのですが、IModelBinderの実装クラスを入れ替えるのは至難の業なので、こういう仕様になってしまうんだということを認識しておいた方がよいかと思います。 私はてっきり空文字がバインドされるものだと思っていて、カスタムクラスへのバインドで見事にはまりました。

ちなみに、Request.Form["TestInput1"]を取得すると空文字になっています。 なんとなくASP.NET MVC3 Frameworkのバグに見えてしまうのですが。。。 ともかくカスタムクラスにバインドする場合はnullがバインドされる可能性を十分に考慮してコーディングするのが現状の対応策でしょう。 モデルクラスのプロパティの実装で、nullが返却されないようにするなどの対応は考慮しておいた方がよさそうですね。

参考:モデルバインディングのカスタマイズ

既定のIValueProvider実装クラスは全てsealedに設定されているため、独自の機能をこれらのクラスに追加することはできません。 よってモデルバインディングの機構に独自の処理を追加するにはIValueProviderの実装クラスを自分で作成する必要があります。 もちろんIModelBinderを実装することも可能ですが、ASP.NET MVC Frameworkのアップデートに楽に対応したいならIValueProviderを実装したほうがよいと思います。

IValueProviderのインスタンスは、System.Web.Mvc.ValueProviderFactory抽象クラスの実装クラスによって生成されます。 よって既定のIValueProviderにも、それぞれ対応するValueProviderFactoryの実装クラスが存在します(System.Web.MVC名前空間にいます。詳細は割愛。)。

ValueProviderFactoryのインスタンスはSystem.Web.Mvc.ValueProviderFactories静的クラスが管理しています。 このクラスのFactoriesプロパティから、DefaultModelBinderが使用するValueProviderFactory実装クラスのインスタンスを取得できます。 逆に言うと、モデルバインディングをカスタマイズしたい場合は以下のステップを踏むことになります。

  1. IValueProviderの実装クラスを作成する
  2. ValueProviderFactoryの実装クラスを作成して、上記IValueProviderのインスタンスを返すように実装する
  3. Global.asaxのApplication_Startで、ValueProviderFactories.Factoriesに上記ValueProviderFactoryのインスタンスを生成して登録する

このような実装をおこなうことで、モデルバインディングの機構をカスタマイズすることができます。 IValueProviderの実装方法についてはいつか別稿でかければいいな。

スポンサーサイト
2012-01-25 : 未分類 : コメント : 0 : トラックバック : 0
Pagetop
コメントの投稿
非公開コメント

Pagetop
ホーム  prev »

プロフィール

masatsuna

筆者:某システム会社のダメSE
競技スキーをこよなく愛するダメSEです。

月別アーカイブ

検索フォーム

ブロとも申請フォーム

この人とブロともになる

QRコード

QR
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。