しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

DropDownListFor と ViewBag を組み合わせた時の問題

ASP.NET MVC でドロップダウンリスト周りを良い感じに扱う - しばやん雑記 で ViewBag に DropDownListFor で指定したプロパティと同じ名前で SelectList を入れておくと良い感じに使ってくれると書きましたが、実際に試していると 1 点だけ問題が出てきました。

その問題とはページを表示したときに初期値を設定しておきたい場合です。実際に再現したコードを載せておきます。

public class FormModel
{
    public int Month { get; set; }
    public int Day { get; set; }
}

public class HomeController : Controller
{
    public ActionResult Index()
    {
        ViewBag.Month = new SelectList(Enumerable.Range(1, 12));
        ViewBag.Day = new SelectList(Enumerable.Range(1, 31));

        var formModel = new FormModel
        {
            Month = DateTime.Today.Month,
            Day = DateTime.Today.Day
        };

        return View(formModel);
    }
}

トップページにアクセスした時に、今日の日付を初期値として設定するだけのアクションです。ドロップダウンリストのために ViewBag に SelectList を入れています。

次は表示されるビューのコードです。

<h2>Index</h2>

@Html.DropDownListFor(model => model.Month, null)
/
@Html.DropDownListFor(model => model.Day, null)

特に説明も不要だと思いますが、DropDownListFor を使ってドロップダウンリストを表示させるだけのコードです。2 つ目の引数を null にして ViewBag から SelectList を取得してもらうようにしています。

さて、これで実行した時にはどうなると思いますか?普通なら今日の日付がドロップダウンリストで選択されていると思いますよね?

f:id:shiba-yan:20130207173819p:plain

残念ながら、モデルとして渡した初期値は利用されていません。

何でこんなことが起こるのかと思ったので DropDownListFor のソースコードを読んでみたところ、何故か初期値としてモデルの値を使わないようになっていました。実際のコードを一部分だけ引っ張ってきました。

object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));

// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData && defaultValue == null && !String.IsNullOrEmpty(name))
{
    defaultValue = htmlHelper.ViewData.Eval(name);
}

defaultValue が初期値となるのですが、何故か ModelState と ViewData からしか取得していません。ModelState や ViewData は基本的に POST が実行された時にしかセットされないので、これが原因のようです。

せっかく引数で ModelMetadata が渡されているので、以下のようにコードを修正することで初期値としてモデルの値を使うことが出来るようになるはずです。

object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));

// metadata が null 以外の場合にはラムダが使われているので、メタデータからモデルの値を引っ張ってくる
if (metadata != null && defaultValue == null)
{
    defaultValue = metadata.Model;
}

// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData && defaultValue == null && !String.IsNullOrEmpty(name))
{
    defaultValue = htmlHelper.ViewData.Eval(name);
}

残念ながら、今の開発では HTML ヘルパー自体に修正を入れることは非常に困難なので、引数として selectList を取らないようにした同名の拡張メソッドを追加して対応しました。