読者です 読者をやめる 読者になる 読者になる
ようこそ。睡眠不足なプログラマのチラ裏です。

いまさらASP.NET MVCのモデルバインダ入門あれこれ。MEFのカスタムエクスポートプロバイダーは設計の幅を広げる。自動拡張型カスタムモデルバインダプロバイダーを作ろう。

プログラミング C# MEF ASP.NET MVC3


http://www.asp.net/



ASP.NET MVC4 Betaがリリースされまして、WebAPIいいね!な今日この頃。誰が言ったか、これから求められるIT技術は、Web、クラウド、関数型言語の三本柱らしいです。とは言っても、世の中にはさまざまな技術が溢れています。.NETerなわたしは月並みですが、ASP.NET MVCWindows Azure、F#を追いかけるつもりです。まぁ、日進月歩の業界ですし、わたし自身飽きっぽかったりするので来年には違うことを言っているかもしれません。最近の私はと言えば、月9ドラマ「ラッキーセブン」でメカオタ少女茅野メイ役を演じている入来茉里さんのファンになりました。スピンオフドラマの「敷島☆珈琲〜バリスタは見た!?〜」も面白い。これからブレイクすること間違いありません。



それはさておき、ASP.NET MVC関連の記事はだんだんと増えてきていますが、なぜか基本中の基本であるカスタムモデルバインダですとか、カスタムモデルバインダプロバイダーに関する記事があまりにも少ない。少なすぎて困っているASP.NET MVC入門者も少なくないと聞いています(要出典)。誰かの役に立つかもしれないということで、いまさらながらASP.NET MVC3のモデルバインダ入門あれこれについてちょっと書いておきます。



このエントリーの主な話題。わりと盛りだくさん。

・カスタムモデルバインダについて
・カスタムモデルバインダプロバイダーについて
Base64でシリアル化可能なモデルと、その汎用モデルバインダについて
・カスタムモデルバインダでアノテーション検証を有効にする
・MEFのカスタムエクスポートプロバイダーについて
・MEFを用いた自動拡張型カスタムモデルバインダプロバイダーについて
・IModelBinderProviderインターフェイスがイケてない説

この記事のサンプルコード一式はSkyDriveへあげておきます。



すてきなモデルバインダ

ASP.NET MVC にはモデルバインダという仕組みがあり、比較的新しいMVCフレームワークで採用されていて、たとえばJavaScript製のMVCフレームワークなんかでもよく採用されているデータバインド手法です。ASP.NET MVCでは、モデルバインダと呼ばれるクラスでリクエストデータ等を使って厳密に型付けされたオブジェクトを作成して、ルーティングやクエリ、フォームパラメータなどに、コントローラーのアクションに対するパラメータの型とのバインディングが管理されます。同名のパラメータについてデータバインドを試みてコントローラのアクションを単純化してくれるし、コントローラー内に「値の変換を行う」というノイズとなる処理がなくなるので、開発者はコントローラー本来の役割の実装に集中できるようなります。素敵ですね。モデルバインディングを実際に実行するのはSystem.Web.Mvc.IModelBinderを実装したクラスで、既定ではSystem.Web.Mvc.DefaultModelBinderクラスが適用されます。この既定で動作するバインダは、文字や数値など.NETで扱う基本的な型や、アップロードされたファイルなど様々な型に対応しています。小規模またはシンプルなシナリオでは、この既定のモデルバインダが自動的に基本的な型をバインドしてくれるので、この動作について特別意識することはあまりないでしょう。ただ、世の中そんなにあまくないのが現実です。大規模または複雑なシナリオでは、既定のバインディングでは十分ではないこともあるでしょう。そのような場合、カスタムモデルバインダ(ModelBinderの拡張)を作成することになります。



既定のモデルバインダが実際にどんな働きをしてくれるのかを一目でわかるように書くと、

[HttpPost]
public ActionResult Create()
{
	var customer = new Customer() 
	{
		CustomerId = Int32.Parse(Request["customerId"]), 
		Description = Request["description"], 
		Kind = (CustomerKind)Enum.Parse(typeof(CustomerKind), Request["kind"]), 
		Name = Request["name"], 
		Address = Request["address"]
	};

	// …

	return View(customer);
};


既定のDefaultModelBinderが処理できる範囲内であれば、上記のような煩雑な型の変換処理をまったく書かなくてよくて、下記のようにシンプルに書けるようになります。

public ActionResult Create(Customer customer) 
{ 
	// … 

	return View(customer);
}


モデルバインダって、とてもかわいいですね。はい。って、ASP.NET MVC3を使ってプログラミングをしている人には当たり前のことでしたね。



モデルバインダの拡張

さて、「大規模または複雑なシナリオでは、既定のバインディングでは十分ではないこともあるでしょう。」と前述しました。そのようなシナリオでは、モデルバインダの拡張、すなわち独自にカスタムモデルバインダを作成することで、さまざなシナリオに対応することができます。



モデルバインダの拡張の方法としては、IModelBinderインターフェイスを実装するか、もしくはIModelBinderを実装している既定のDefaultModelBinderクラスを継承して実装します。IModelBinderインターフェイスを実装する方法の場合は、object BindModel(...)メソッドを実装するだけというシンプル設計。


DefaultModelBinderを継承して作る場合の主な拡張ポイントとしては以下のものがあり、適宜必要なものをオーバーライドして実装します。

object BindModel(...);			// モデルバインド実行
object CreateModel(...);		// モデル型オブジェクト生成
bool OnModelUpdating(...);		// モデル更新開始
void OnModelUpdated(...);		// モデル更新完了
bool OnPropertyValidating(...);		// プロパティ検証開始
void OnPropertyValidated(...);		// プロパティ検証完了

また、拡張した自作のモデルバインダはいくつかの異なるレベルで登録することができて、これにより非常に柔軟にバインディング方法を選択できます。

// Application_Start()で登録する方法
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
ModelBinders.Binders.Add(typeof(MyModel), new CustomModelBinder());

// Actionの引数に属性で指定する方法
[ModelBinder(typeof(CustomModelBinder))]


他にも、ModelBinderProviderを登録して対応することもできます。これについては後程述べます。



カスタムモデルバインダを作ろう


ではカスタムモデルバインダを作成してみましょう。以下のようなユーザー定義のモデルを含む単純なViewModelをバインドしたい場合を考えます。

namespace ModelBinderSample.Models.ViewModel
{
    public class SampleViewModel0
    {
        public Sample0 Child { get; set; }
    }
}
using System.ComponentModel.DataAnnotations;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Models
{
    public enum Hoge
    {
        Test1,
        Test2,
        Test3
    }

    public class Sample0 
    {
        public Hoge Hoge { get; set; }

        [Display(Name = "ただのプロパティ")]
        public string NomalProperty { get; set; }
    }
}


IModelBinderインターフェイスを実装する方法を試してみましょう。例えば、下記サンプルのように実装することができます。object BindModel(...)メソッドの基本実装は、リクエストを適切な型に変換して返してあげる処理を書くだけです。実用性はありませんが下記サンプルのように値を直接編集したりもできますし、他にも値を検証してエラーメッセージを追加したりすることもできます。

using System;
using System.Web;
using System.Web.Mvc;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Models.ModelBinder
{
    public class SampleViewModel0Binder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            HttpRequestBase request = controllerContext.HttpContext.Request;

            var model = new Sample0()
            {
                Hoge = (Hoge)Enum.Parse(typeof(Hoge), request.Form.Get("Child.Hoge"), false),
                NomalProperty = request.Form.Get("Child.NomalProperty") + "だってばよ!",
            };

            return new SampleViewModel0() { Child = model };
        }
    }
}


ビュー:Sample0/Index.cshtml

@using ModelBinderSample.Models
@using ModelBinderSample.Models.ViewModel
@model SampleViewModel0
           
@{
    ViewBag.Title = "Sample0";
}

<h2>@ViewBag.Message</h2>

@using (Html.BeginForm("Index", "Sample0"))
{

    @Html.TextBoxFor(vm => vm.Child.NomalProperty, new { @style = "width: 350px;" }) 
    @Html.HiddenFor(vm => vm.Child.Hoge)

    <br />    
    <input type="submit" value="送信" />
}

コントローラー:Sample0Controller.cs

using System;
using System.Web.Mvc;
using ModelBinderSample.Models;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Controllers
{
    public class Sample0Controller : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "ASP.NET MVC へようこそ";


            var vm = new SampleViewModel0()
            {
                Child = new Sample0()
                {
                    Hoge = Models.Hoge.Test2,
                    NomalProperty = "うずまきナルト",
                }
            };

            return View(vm);
        }

        [HttpPost]
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Index(SampleViewModel0 vm)
        {
            ViewBag.Message = "ASP.NET MVC へようこそ";

            if (!ModelState.IsValid)
            {
                return View(vm);
            }

            return View(vm);
        }

        public ActionResult About()
        {
            return View();
        }
    }
}

モデルバインダの登録

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    // Add ModelBinder
    ModelBinders.Binders.Add(typeof(SampleViewModel0), new SampleViewModel0Binder());

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}


内容はお粗末ですが、カスタマイズはできました。もう少し踏み込んだカスタマイズについては後半で。


ModelBinderProviderの拡張 : カスタムモデルバインダプロバイダー

モデルの型ごとに適切なモデルバインダを供給するクラス。それがモデルバインダプロバイダー。もっと噛み砕いて言うと、「このモデルの型の場合は、このモデルバインダを使ってバインディングしてくださいね〜」って情報を供給してくれるクラスです。カスタムモデルバインダプロバイダーは、IModelBinderProviderインターフェイスを実装して作ることができます。



SampleViewModel0モデルのカスタムモデルバインダプロバイダーを実装サンプル


SampleViewModel0BinderProvider.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ModelBinderSample.Models.ModelBinder;
using ModelBinderSample.Models.ViewModel;
using ClassLibrary1;

namespace ModelBinderSample.Models.ModelBinderProvider
{
    public class SampleViewModel0BinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(Type modelType)
        {
            if (modelType == typeof(SampleViewModel0))
                return new SampleViewModel0Binder();
            return new DefaultModelBinder();
        }
    }
}

このサンプルでは、型がSampleViewModel0であるとき、SampleViewModel0Binderを返し、それ以外の型のときは既定のモデルバインダを返しているだけなので、プロバイダーとしてはあまり意味がありません。通常は、さまざまなモデルの型に応じて異なるモデルバインダを返すようなモデルバインダプロバイダーを作ります。


モデルバインダプロバイダーの登録

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    // Add ModelBinderProvider
    ModelBinderProviders.BinderProviders.Add(new SampleViewModel0BinderProvider());

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}



Base64でシリアル化可能なモデルと、その汎用モデルバインダ

もう少し踏み込んだカスタムモデルバインダの例を見てみます。例としてはあまりよろしくはないですが、こういう実装もできるんだよというサンプルとして、Base64でシリアル化可能なModelをバインドするための汎用的なモデルバインダを作ってみましょう。例えば、ViewModelにユーザー定義の型のプロパティを含むような場合、当然 DefaultModelBinder ではそのような型をバインドできませんので、コントローラーのアクションパラメータとうまくバインドできずに、そのViewModelのプロパティにはnullが設定されてしまいます。そこで任意の型についてBase64形式でシリアル化可能なモデルをバインドするような、汎用的なカスタムモデルバインダを考えてみます。



ひどく曖昧な抽象化ですが、まずシリアル化可能なモデルであることを表すインターフェイスを定義します。BindTypeプロパティでは、バインドする型(つまりはモデル自身の型)を返すように実装します。ToStringメソッドでは、Base64エンコードした文字列を返すように実装します。


ISerializableModel.cs

using System;

namespace ClassLibrary1
{
    public interface ISerializableModel
    {
        Type BindType { get; }
        string ToString();
    }
}



そのインターフェイスを実装しただけの抽象クラス。相変わらず曖昧模糊。


AbustractSerializableModel.cs

using System;

namespace ClassLibrary1
{
    [Serializable]
    public abstract class AbustractSerializableModel : ISerializableModel
    {
        public abstract Type BindType { get; }
        public abstract override string ToString();
    }
}


Base64でシリアル化可能なモデルのカスタムモデルバインダを実装します。下記サンプルのように、自身の型のModelMetadataから、ModelValidatorを取得して自身の型のバリデーションの処理も行うように実装しておくと、カスタムモデルバインダでもアノテーション検証がされるようになり、ViewModelに入れ子となっている場合でも検証を有効にするよう実装することもできます。これは、今回の実装にかかわらず様々な実装で使える方法なので覚えておいて損はないでしょう。


SerializeableModelBinder{T}.cs

using System.Web.Mvc;

namespace ModelBinderSample.Models.ModelBinder.Binder
{
    public class SerializeableModelBinder<T> : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelMetadata.ModelType != typeof(T))
                return base.BindModel(controllerContext, bindingContext);

            var serializedModel = controllerContext.HttpContext.Request[bindingContext.ModelName];
            var model = Serializer.Deserialize(serializedModel);

            ModelMetadata modelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType());
            ModelValidator compositeValidator = ModelValidator.GetModelValidator(modelMetadata, controllerContext);

            foreach (ModelValidationResult result in compositeValidator.Validate(null))
                bindingContext.ModelState.AddModelError(bindingContext.ModelName + "." + result.MemberName, result.Message);      

            return model;
        }
    }
}

Base64シリアライズとデシリアライズ
Serializer.cs

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ModelBinderSample
{
    public static class Serializer
    {
        public static string Serialize(object obj)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                var bf = new BinaryFormatter();
                bf.Serialize(stream, obj);
                return Convert.ToBase64String(stream.GetBuffer());
            }
        }

        public static object Deserialize(string subject)
        {
            using (var stream = new MemoryStream(Convert.FromBase64String(subject)))
            {
                var bf = new BinaryFormatter();
                return bf.Deserialize(stream);
            }
        }
    }
}

Sample1.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using ClassLibrary1;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Models
{
    [Serializable]
    public class Sample1 : AbustractSerializableModel
    {
        public override Type BindType
        {
            get { return this.GetType(); }
        }

        [Display(Name="ただのプロパティ")]
        public string NomalProperty { get; set; }

        public string[] ParamString { get; set; }

        public int[] ParamInt { get; set; }

        public Hoge Hoge { get; set; }

        public override string ToString()
        {
            Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
            return Serializer.Serialize(this);
        }
    }
}

Sample2.cs

using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using ClassLibrary1;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Models
{
    [Serializable]
    public class Sample2 : AbustractSerializableModel
    {
        public override Type BindType
        {
            get { return this.GetType(); }
        }

        [Display(Name = "必須なプロパティ")]
        [Required(ErrorMessage = "「{0}」は、必須だってばよ!")]
        public string RequiredProperty { get; set; }

        public string[] ParamString { get; set; }

        public int[] ParamInt { get; set; }

        public Hoge Hoge { get; set; }

        public override string ToString()
        {
            Contract.Ensures(!string.IsNullOrWhiteSpace(Contract.Result<string>()));
            return Serializer.Serialize(this);
        }
    }
}


Sample3.cs

using System.ComponentModel.DataAnnotations;
using ModelBinderSample.Models.ViewModel;

namespace ModelBinderSample.Models
{
    public class Sample3 
    {

        [Display(Name = "入力必須なやつ")]
        [Required(ErrorMessage = "「{0}」は、必須だってばよ!")]
        public string RequiredProperty { get; set; }

        public string[] ParamString { get; set; }

        public int[] ParamInt { get; set; }

        public Hoge Hoge { get; set; }
    }
}


モデルバインダの登録

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    // Add ModelBinder
    ModelBinders.Binders.Add(typeof(Sample1), new SerializeableModelBinder<Sample1>());
    ModelBinders.Binders.Add(typeof(Sample2), new SerializeableModelBinder<Sample2>());
    ModelBinders.Binders.Add(typeof(Sample3), new SerializeableModelBinder<Sample3>());

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}


Sample3クラスは、SerializableでもなければISerializableModelインターフェイスも実装していないので、SerializeableModelBinderクラスによってバインドされませんが、Base64シリアライズできるモデルについては、汎用的なモデルバインダによってバインディングされます。ご利用は計画的に。何が言いたいかというと、必ずしもモデルの型とモデルバインダは1対1の関係というわけではないというわけです。また、「モデルの型」という言い方をしていますが、型以外の判定手段(インスタンスそのものの値や状態)でバインディング方法を変えるという方法を取ることもできます。そこは設計次第です。腕の見せ所ですね。


さて、実装サンプルSerializeableModelBinderクラスを用いることで、Base64シリアライズできるモデルについて汎用的にバインディングできるようになりました。しかしながら、Sample4,Sample5...と新しくシリアライズ可能なクラスを作るたびに、Application_Start()にて、対象となるモデルに対してモデルバインダを登録しなければならないというのは非常に面倒くさいです。われわれ開発者は、自動化できることならなるべく自動化したいという怠け者。



そこで、MEF(Managed Extensibility Framework)を用いて自動拡張型カスタムモデルバインダプロバイダーを作ることを考えてみます。



ExportProviderの拡張 : 任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダー

さっそく「MEFを用いた自動拡張型カスタムモデルバインダプロバイダー」の作成と行きたいところなんですが、その前に下準備が必要となります。ISerializableModelインターフェイスを実装している具象クラスをコントラクトとするMEFエクスポートが必要になるからです。そのために、任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダーを作成する必要があります。前回のエントリーではWindows AzureでBlobストレージからMEFのパーツを検索できるカスタムCatalogを紹介しました。今回は、Catalogに比べて、よりピンポイントな条件でエクスポートができる、カスタムエクスポートプロバイダーを紹介します。



MEFの入門記事はわかりやすいものがいくつかありますが、入門よりももう少し踏み込んだ情報はあまりありません。海外記事を含めてもカスタムカタログやカスタムエクスポートプロバイダー等の解説記事や簡単なサンプルは決して多くはありません。MEF(Managed Extensibility Framework)を積極的に使おうと考えた場合、カタログやエクスポートプロバイダーのカスタマイズは必須です。オブジェクト指向なスタイルの開発においては、インターフェイスによる多態は日常茶飯事ですし、任意のインターフェイスの実装をコントラクトとするエクスポートプロバイダーとか、欲しくなるのは自然な流れです。ということで、シンプルなサンプルコードを以下に示します。



InterfaceExportProvider{T}.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using ClassLibrary1;

namespace ClassLibrary2
{
    public class InterfaceExportProvider<T> : ExportProvider
    {
        private readonly IList<InterfaceExportDefinition> exportDefinitions = new List<InterfaceExportDefinition>();

        public InterfaceExportProvider() : this(() => Assembly.GetExecutingAssembly().GetTypes(), t => true) 
        { 
        }
        public InterfaceExportProvider(Func<Type, bool> predicate) : this(() => Assembly.GetExecutingAssembly().GetTypes(), predicate) 
        {
            Contract.Requires(predicate != null);
        }

        public InterfaceExportProvider(Func<Type[]> factory, Func<Type, bool> predicate)
        {
            Contract.Requires(factory != null);

            var types = factory()
                       .Where(t => !t.IsAbstract)
                       .Where(t => !t.IsInterface)
                       .Where(t => predicate(t));
            ComposeTypes(types);
        }

        protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
        {
            Contract.Ensures(0 <= this.exportDefinitions.Count);
            return exportDefinitions.Where(ed => definition.ContractName == ed.ContractName)
                                    .Select(ed => new Export(ed, () => Util.New(ed.ServiceType)));
        }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(typeof(T).IsInterface);
        }

        private void ComposeTypes(IEnumerable<Type> serviceTypes)
        {
            Contract.Requires(serviceTypes != null);

            serviceTypes
                .Where(x => !x.IsAbstract)
                .Select(type => new { Type = type, InterfaceType = type.GetInterfaces().Where(t => t == typeof(T)).SingleOrDefault()})
                .Where (x  => x.InterfaceType != null).ToList()
                .ForEach(x =>
                {
                    var metadata = new Dictionary<string, object>();
                    metadata[CompositionConstants.ExportTypeIdentityMetadataName] = AttributedModelServices.GetTypeIdentity(x.Type);
                    var contractName = AttributedModelServices.GetContractName(x.InterfaceType);
                    var exportDefinition = new InterfaceExportDefinition(contractName, metadata, x.Type);
                    exportDefinitions.Add(exportDefinition);
                });
        }
    }
}

例えば上記のクラスをデフォルトコンストラクタインスタンス化した場合、現在実行中のコードを格納しているアセンブリ内のうち、ジェネリックタイプTで指定したインターフェイスをコントラクトとする型についてエクスポートを行います。そういうExportプロバイダー実装です。要するに、ジェネリックタイプTで指定したインターフェイスを実装している具象クラスを検索してオブジェクトグラフのファクトリを行うようなプロバイダーということです。これがあると、オブジェクト指向プログラミングで当たり前のインターフェイスによる多態をひとまとめに"[ImportMany(typeof(インターフェイス))]"というように、Exportできるので嬉しいというわけです。




上記InterfaceExportProviderクラスに合わせて、そのようなコントラクトを満たすExportオブジェクトを表すカスタムExportDefinitionも定義も必要となります。こちらは、ContractNameプロパティとMetadataプロパティをoverrideして実装を上書いているだけのなんの芸もない実装ですので、難しいことは何もないですね。

InterfaceExportDefinition.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics.Contracts;

namespace ClassLibrary2
{
    public class InterfaceExportDefinition : ExportDefinition
    {
        private readonly string _contractName;
        private readonly Dictionary<string, object> _metaData;

        public InterfaceExportDefinition(string contractName, Dictionary<string, object> metaData, Type type)
        {
            Contract.Requires(metaData != null);
            Contract.Requires(type != null);
            Contract.Ensures(this._contractName == contractName);
            Contract.Ensures(this._metaData == metaData);

            this._contractName = contractName;
            this._metaData = metaData;
            ServiceType = type;
        }

        public Type ServiceType { get; private set; }

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(this._metaData != null);
        }

        public override IDictionary<string, object> Metadata
        {
            get 
            {
                Contract.Ensures(this._metaData != null);
                Contract.Ensures(Contract.Result<IDictionary<string, object>>() == this._metaData);
                return this._metaData; 
            }
        }

        public override string ContractName
        {
            get 
            {
                Contract.Ensures(Contract.Result<string>() == this._contractName);
                return this._contractName; 
            }
        }
    }
}


これで、任意のインターフェイスの実装をコントラクトとするカスタムエクスポートプロバイダーができました。オブジェクト指向においては、インターフェイスによる多態は日常茶飯事ですので利用場面はたくさんありそうですね。


MEFを用いた自動拡張型カスタムモデルバインダプロバイダー

では作成したInterfaceExportProviderクラスを用いて、自動拡張してくれるカスタムモデルバインダプロバイダーを実装します。ImportMany属性で、コントラクト型でISerializableModelを指定することで、ISerializableModelインターフェイスを実装している具象クラスをコントラクトとしたエクスポートがなされるので、ISerializableModelインターフェイスを実装しているモデルについて、適切にモデルバインディングしてくれるという寸法です。CompositionContainerフィールドはIDisposableですので、忘れずにIDisposableのイディオムを用いて綺麗にガベコレしてくれるように実装しましょう。


SerializeableModelBinderProvider.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Web.Mvc;
using ClassLibrary1;
using ClassLibrary2;
using ModelBinderSample.Models.ModelBinder;
using System.Collections.Concurrent;

namespace ModelBinderSample.Models.ModelBinderProvider
{
    public class SerializeableModelBinderProvider : IModelBinderProvider, IDisposable
    {
        private bool disposed;
        private readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();

        [ImportMany(typeof(ISerializableModel))]
        private IEnumerable<Lazy<ISerializableModel>> _serializableModels = null;
        private CompositionContainer _Container = null;

        private SerializeableModelBinderProvider()
        {
            this.disposed = false;
        }

        public SerializeableModelBinderProvider(Func<Type[]> factory) : this()
        {
            ComposeParts(factory);
        }

        public IModelBinder GetBinder(Type modelType)
        {
            this.ThrowExceptionIfDisposed();

            if (CanBind(modelType))
            {
                var modelBinderType = _cache.GetOrAdd(modelType, typeof(SerializeableModelBinder<>).MakeGenericType(modelType));
                return (IModelBinder)Activator.CreateInstance(modelBinderType);
            }
            return null;
        }

        public bool CanBind(Type modelType)
        {
            if (_cache.ContainsKey(modelType))
                return true;

            var count = _serializableModels.Where(m => m.Value.BindType == modelType).Count();
            if (count > 0)
                return true;
            return false;
        }

        protected void ThrowExceptionIfDisposed()
        {
            if (this.disposed)
            {
                throw new ObjectDisposedException(this.GetType().ToString());
            }
        }

        public void ComposeParts(Func<Type[]> factory)
        {
            this.ThrowExceptionIfDisposed();

            var provider = new InterfaceExportProvider<ISerializableModel>(factory, x => x.IsSerializable);
            _Container = new CompositionContainer(provider);
            _Container.ComposeParts(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            lock (this)
            {
                if (this.disposed)
                {
                    return;
                }

                this.disposed = true;

                if (disposing)
                {
                    if (_Container != null)
                    {
                        _Container.Dispose();
                        _Container = null;
                    }
                }
            }
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
}


このような汎用的なカスタムモデルバインダプロバイダーを作成することで、Sample4, Samole5...と、シリアル化可能なクラスを次々と定義していくだけで、自動的に拡張されていくカスタムエクスポートプロバイダーを作成することができるというわけです。MEFはユーザーの目に見えるような機能面での拡張のみならず、開発視点においても確実に設計の幅を広げてくれます。MEFは.NET Framework4標準ですので、臆することなくガンガン使っていけるのがうれしいですね。



IModelBinderProviderインターフェイスがイケてない説

まず、System.Web.Mvc.IModelBinderProviderインターフェイスの定義をご覧いただきましょう。

public interface IModelBinderProvider
{
	IModelBinder GetBinder(Type modelType);
}


モデルの型を引数で受け取り、適切なモデルバインダを返すだけのGetBinderメソッドを持つ、とてもシンプルなインターフェイスです。あまりにもシンプルすぎて、モデルバインダプロバイダーがどんなモデルの型を対象としたプロバイダーなのか外部から知るすべもありません。GetBinderメソッドの戻り値が null だったら、次のモデルバインダプロバイダーに処理を委譲する作りになっているので、複数のカスタムモデルバインダプロバイダーが協調して動作するようにするには、サポートしないモデルの型の場合に必ず null を返さなければなりません。「該当する結果がない場合にnullを返して、戻り値側でそれがnullだったら次の処理を...」という仕様はあんましイクナイ(・Α・)と思います。もっと別の方法もあっただろうに...。




あと、おまけ。
Util.cs

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;

namespace ClassLibrary1
{
    public static class Util
    {
        public static T New<T>()
        {
            Type type = typeof(T);
            Func<T> method = Expression.Lambda<Func<T>>(Expression.Block(type, new Expression[] { Expression.New(type) })).Compile();
            return method();
        }

        public static object New(Type type)
        {
            Func<object> method = Expression.Lambda<Func<object>>(Expression.Block(type, new Expression[] { Expression.New(type) })).Compile();
            return method();
        }

        public delegate TInstance ObjectActivator<TInstance>(params object[] args);
        public static ObjectActivator<TInstance> GetActivator<TInstance>(ConstructorInfo ctor)
        {
            Type type = ctor.DeclaringType;
            ParameterInfo[] paramsInfo = ctor.GetParameters();

            ParameterExpression param = Expression.Parameter(typeof(object[]), "args");
            Expression[] argsExp = new Expression[paramsInfo.Length];

            for (int i = 0; i < paramsInfo.Length; i++)
            {
                Expression index = Expression.Constant(i);
                Type paramType = paramsInfo[i].ParameterType;
                Expression paramAccessorExp = Expression.ArrayIndex(param, index);
                Expression paramCastExp = Expression.Convert(paramAccessorExp, paramType);
                argsExp[i] = paramCastExp;
            }

            NewExpression newExp = Expression.New(ctor, argsExp);
            LambdaExpression lambda = Expression.Lambda(typeof(ObjectActivator<TInstance>), newExp, param);

            ObjectActivator<TInstance> compiled = (ObjectActivator<TInstance>)lambda.Compile();
            return compiled;
        }
    }
}

モデルバインダプロバイダーの登録

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    // Add ModelBinderProvider
    ModelBinderProviders.BinderProviders.Add(new SampleViewModel0BinderProvider());
    ModelBinderProviders.BinderProviders.Add(new SerializeableModelBinderProvider(() => Assembly.GetExecutingAssembly().GetTypes()));

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);
}


さてコード中心の記事でしたが、ASP.NET MVC3のカスタムモデルバインダとカスタムモデルバインダプロバイダーについてのサンプルプログラムと、MEFのカスタムエクスポートプロバイダーを利用した自動拡張型のコンポーネント設計の手法について見てきました。モデルバインダの仕組みはASP.NET MVC3のコアコンポーネントのひとつであり基本中の基本ですので、既定のDefaultModelBinderのみに頼るのではなく、このあたりの仕組みや拡張・設計ポイントはしっかり押さえておきたいところです。長々と書きましたが、何かの参考になれば幸いです。


F#はちょい充電中。