アーキテクチャをスマートに。

株式会社ネオジニア代表。ITアーキテクトとしてのお仕事や考えていることなどをたまに綴っています。(記事の内容は個人の見解に基づくものであり、所属組織を代表するものではありません)

ViewBagに匿名型を入れるとRuntimeBinderException

ViewBag に匿名型を入れて View 側で使おうとすると、「'object' に 'Xxxxx' の定義がありません」みたいなエラー(RuntimeBinderException)になってしまいます。

開発環境は以下の通り。

この問題について調べていたところ、StackOverFlowのスレッド に同じ現象が投稿されており、それを参考にして解決できたのできたのですが、日本語の情報が全くなくだいぶ苦労したのでここに記録しておきます。

問題のコードは以下の通り。
コントローラ側で匿名クラスをViewBagに入れ、ビュー側でそれを見に行く、というだけのことです。

/Controllers/HomeController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication8.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            ViewBag.Message = "Hello";

            ViewBag.Object1 = new
            {
                String1 = "World"
            };

            return View();
        }

    }
}

/Views/Home/Index.cshtml

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>@ViewBag.Message</p>

<p>@ViewBag.Object1.String1</p>
実行結果

リフレクションを使うと?

試しにリフレクションを使って匿名型のプロパティを取り出してみたところと、問題なく取れました。

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>@ViewBag.Message</p>

@{
    Type t = ViewBag.Object1.GetType();
    var string1 = t.GetProperty("String1").GetValue(ViewBag.Object1);
}
<p>@string1</p>

でもこれではメンドクサ過ぎて解決になってない。
ちゃんとプロパティは入ってるんだから普通に参照できてほしいな〜

解決策

で、解決策として紹介されていたのは、dynamic オブジェクトを ExpandoObject に変換する方法でした。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Dynamic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication8.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/

        public ActionResult Index()
        {
            ViewBag.Message = "Hello";

            ViewBag.Object1 = new
            {
                String1 = "World"
            }.ToExpando();  // ViewBagに入れるとき匿名型をExpandoObjectに変換!

            return View();
        }

    }

    public static class Extentions
    {
        public static ExpandoObject ToExpando(this object anonymousObject)
        {
            IDictionary<string, object> expando = new ExpandoObject();
            foreach (PropertyDescriptor propertyDescriptor in TypeDescriptor.GetProperties(anonymousObject))
            {
                var obj = propertyDescriptor.GetValue(anonymousObject);
                expando.Add(propertyDescriptor.Name, obj);
            }

            return (ExpandoObject)expando;
        }
    }
}

こうすることによって、View側で ViewBag.Object1.String1 がエラーにならず、期待通りの動きになります。

原因

詳細説明は引用元のサイトを見て頂きたいのですが、要するに Controller と View のコンパイル単位が異なるためにそうなってしまうんだそうな。
なのでViewBagじゃなくてViewDataを使っても同じ。ControllerからViewに匿名型を渡そうとするとダメ、ということでしょうね。
これ例えば、LINQ を使ってクエリ結果をそのままViewに渡したりすると途端にこの現象でつまづいてしまうわけですが、なんであまり情報がないんだろう?
みんなどうやってんの?