Movatterモバイル変換


[0]ホーム

URL:


Bug Catharsis

ようこそ。睡眠不足なプログラマのチラ裏です。

この広告は、90日以上更新していないブログに表示しています。

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


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{publicclass SampleViewModel0    {public Sample0 Child { get; set; }    }}
using System.ComponentModel.DataAnnotations;using ModelBinderSample.Models.ViewModel;namespace ModelBinderSample.Models{publicenum Hoge    {        Test1,        Test2,        Test3    }publicclass Sample0     {public Hoge Hoge { get; set; }        [Display(Name ="ただのプロパティ")]publicstring NomalProperty { get; set; }    }}


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

using System;using System.Web;using System.Web.Mvc;using ModelBinderSample.Models.ViewModel;namespace ModelBinderSample.Models.ModelBinder{publicclass SampleViewModel0Binder : IModelBinder    {publicobject 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") +"だってばよ!",            };returnnew 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{publicclass 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();        }    }}

モデルバインダの登録

protectedvoid 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{publicclass SampleViewModel0BinderProvider : IModelBinderProvider    {public IModelBinder GetBinder(Type modelType)        {if (modelType ==typeof(SampleViewModel0))returnnew SampleViewModel0Binder();returnnew DefaultModelBinder();        }    }}

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


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

protectedvoid 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{publicinterface ISerializableModel    {        Type BindType { get; }string ToString();    }}



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


AbustractSerializableModel.cs

using System;namespace ClassLibrary1{    [Serializable]publicabstractclass AbustractSerializableModel : ISerializableModel    {publicabstract Type BindType { get; }publicabstractoverridestring ToString();    }}


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


SerializeableModelBinder{T}.cs

using System.Web.Mvc;namespace ModelBinderSample.Models.ModelBinder.Binder{publicclass SerializeableModelBinder<T> : DefaultModelBinder    {publicoverrideobject BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)        {if (bindingContext.ModelMetadata.ModelType !=typeof(T))returnbase.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 resultin 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{publicstaticclass Serializer    {publicstaticstring Serialize(object obj)        {using (MemoryStream stream =new MemoryStream())            {                var bf =new BinaryFormatter();                bf.Serialize(stream, obj);return Convert.ToBase64String(stream.GetBuffer());            }        }publicstaticobject 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]publicclass Sample1 : AbustractSerializableModel    {publicoverride Type BindType        {get {returnthis.GetType(); }        }        [Display(Name="ただのプロパティ")]publicstring NomalProperty { get; set; }publicstring[] ParamString { get; set; }publicint[] ParamInt { get; set; }public Hoge Hoge { get; set; }publicoverridestring 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]publicclass Sample2 : AbustractSerializableModel    {publicoverride Type BindType        {get {returnthis.GetType(); }        }        [Display(Name ="必須なプロパティ")]        [Required(ErrorMessage ="「{0}」は、必須だってばよ!")]publicstring RequiredProperty { get; set; }publicstring[] ParamString { get; set; }publicint[] ParamInt { get; set; }public Hoge Hoge { get; set; }publicoverridestring 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{publicclass Sample3     {        [Display(Name ="入力必須なやつ")]        [Required(ErrorMessage ="「{0}」は、必須だってばよ!")]publicstring RequiredProperty { get; set; }publicstring[] ParamString { get; set; }publicint[] ParamInt { get; set; }public Hoge Hoge { get; set; }    }}


モデルバインダの登録

protectedvoid 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()にて、対象となるモデルに対してモデルバインダを登録しなければならないというのは非常に面倒くさいです。われわれ開発者は、自動化できることならなるべく自動化したいという怠け者。


zecl

拡張ポイントの見極めって一言でいっても、ユーザビリティ的な意味での設計の視点と、開発テクニック的な意味での設計の視点の両方がありますよね

2012-02-0316:24:40viaSilver Bird


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



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

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


zecl

任意のインターフェースを指定してExportできるExportProvidorとか、普通にあったら便利っぽいので、誰か実装してくれていないものかな?と思ってちょっと調べたけど、そもそもMEFが流行っていないので期待した俺がバカだったよということで自分で書くしかなかった。

2012-02-1401:10:27via web


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{publicclass InterfaceExportProvider<T> : ExportProvider    {privatereadonly 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);        }protectedoverride 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]privatevoid ObjectInvariant()        {            Contract.Invariant(typeof(T).IsInterface);        }privatevoid 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{publicclass InterfaceExportDefinition : ExportDefinition    {privatereadonlystring _contractName;privatereadonly 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]privatevoid ObjectInvariant()        {            Contract.Invariant(this._metaData !=null);        }publicoverride IDictionary<string,object> Metadata        {get             {                Contract.Ensures(this._metaData !=null);                Contract.Ensures(Contract.Result<IDictionary<string,object>>() ==this._metaData);returnthis._metaData;             }        }publicoverridestring ContractName        {get             {                Contract.Ensures(Contract.Result<string>() ==this._contractName);returnthis._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{publicclass SerializeableModelBinderProvider : IModelBinderProvider, IDisposable    {privatebool disposed;privatereadonly 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);            }returnnull;        }publicbool CanBind(Type modelType)        {if (_cache.ContainsKey(modelType))returntrue;            var count = _serializableModels.Where(m => m.Value.BindType == modelType).Count();if (count >0)returntrue;returnfalse;        }protectedvoid ThrowExceptionIfDisposed()        {if (this.disposed)            {thrownew ObjectDisposedException(this.GetType().ToString());            }        }publicvoid ComposeParts(Func<Type[]> factory)        {this.ThrowExceptionIfDisposed();            var provider =new InterfaceExportProvider<ISerializableModel>(factory, x => x.IsSerializable);            _Container =new CompositionContainer(provider);            _Container.ComposeParts(this);        }protectedvirtualvoid Dispose(bool disposing)        {lock (this)            {if (this.disposed)                {return;                }this.disposed =true;if (disposing)                {if (_Container !=null)                    {                        _Container.Dispose();                        _Container =null;                    }                }            }        }publicvoid Dispose()        {this.Dispose(true);            GC.SuppressFinalize(this);        }    }}


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



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

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

publicinterface 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{publicstaticclass Util    {publicstatic 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();        }publicstaticobject New(Type type)        {            Func<object> method = Expression.Lambda<Func<object>>(Expression.Block(type,new Expression[] { Expression.New(type) })).Compile();return method();        }publicdelegate TInstance ObjectActivator<TInstance>(paramsobject[] args);publicstatic 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;        }    }}

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

protectedvoid 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#はちょい充電中。

検索
参加グループ

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp