2010年07月16日のツイート

Managed Extensibility Framework(MEF)入門 -DirectoryCatalog-

MEFではコンテナーにある領域のパーツを取り込む為の機構として「カタログ」と言う物が用意されています。今回は「あるディレクトリにあるアセンブリパーツを取り込む」為のカタログ『System.ComponentModel.Composition.Hosting.DirectoryCatalog』クラスを取り上げます。DirectoryCatalogクラスはMSDNでこのように説明されています。


DirectoryCatalog は、指定されたディレクトリの内容を解析します。DLL ファイルに含まれている属性付きパーツは抽出され、カタログで利用可能になります。特定の DLL と解析を制限する検索パターンは、GetFiles と同じ構文を使用して指定することもできます


DirectoryCatalogはコンストラクタで指定されたパスを元に、そのパス内に存在しているアセンブリよりパーツを検出します。そのDirectoryCatalogクラスをコンテナーのコンストラクタ引数に指定するとコンテナーはカタログよりパーツを取得しコンテナー内に構成します。

 

directorycatalog_details

 

簡単なサンプルコードを元に見てみましょう。

C# DirectoryCatalog.CSharpプロジェクト
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
 
namespace DirectoryCatalogSample.CSharp
{
    public class Program
    {
        /// <summary>
        /// コンテナー
        /// </summary>
        private CompositionContainer container;
 
        /// <summary>
        /// コンストラク
        /// </summary>
        private Program()
        {
            DirectoryCatalog catalog = new DirectoryCatalog(@"C:\Parts.CSharp");
            this.container = new CompositionContainer(catalog);
        }
 
        /// <summary>
        /// View群
        /// </summary>
        [ImportMany()]
        public IEnumerable<IView> Views { get; set; }
 
        /// <summary>
        /// 実行処理
        /// </summary>
        private void Run()
        {
            //コンテナーより直接サービスを取得する。
            IEnumerable<IView> views = this.container.GetExportedValues<IView>();
            foreach (IView view in views)
            {
                Console.WriteLine(string.Format("タイトルは[{0}]です。", view.GetTitle()));
            }
 
            //自身のインスタンスにサービスをインポートする。
            this.container.ComposeParts(this);
            foreach (IView view in this.Views)
            {
                Console.WriteLine(string.Format("タイトルは[{0}]です。", view.GetTitle()));
            }
        }
 
        /// <summary>
        /// エントリポイント
        /// </summary>
        /// <param name="args"></param>
        public static void Main(string[] args)
        {
            Program program = new Program();
            program.Run();
        }
    }
 
    /// <summary>
    /// IViewインターフェース
    /// </summary>
    public interface IView
    {
        string GetTitle();
    }
}
C# ViewPartsプロジェクト(C:\Parts.CSharpに出力している)
using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using DirectoryCatalogSample.CSharp;
 
namespace ViewParts.CSharp
{
    /// <summary>
    /// メインビュー
    /// </summary>
    [Export(typeof(IView))]
    public class MainView : IView
    {
        public string GetTitle()
        {
            return "Main Window";     
        }
    }
 
    /// <summary>
    /// メッセージダイアログビュー
    /// </summary>
    [Export(typeof(IView))]
    public class MessageDialog : IView
    {
        public string GetTitle()
        {
            return "Message Dialog";
        }
    }
}
VB DirectoryCatalogSample.VBプロジェクト
Imports System
Imports System.ComponentModel.Composition
Imports System.ComponentModel.Composition.Hosting
 
Public Class Program
    ''' <summary>
    ''' コンテナー
    ''' </summary>
    Private container As CompositionContainer
 
    ''' <summary>
    ''' View群
    ''' </summary>
    <ImportMany()> _
    Public Property Views As IEnumerable(Of IView)
 
    ''' <summary>
    ''' コンストラク
    ''' </summary>
    Private Sub New()
        Dim catalog As New DirectoryCatalog("C:\Parts.VB")
        Me.container = New CompositionContainer(catalog)
    End Sub
 
    ''' <summary>
    ''' 処理実行
    ''' </summary>
    Private Sub Run()
        'コンテナーより直接サービスを取得する。
        Dim views As IEnumerable(Of IView) = Me.container.GetExportedValues(Of IView)()
        For Each view As IView In views
            Console.WriteLine(String.Format("タイトルは[{0}]です。", view.GetTitle()))
        Next
 
        '自身のインスタンスにサービスをインポートする。
        Me.container.ComposeParts(Me)
        For Each view As IView In Me.Views
            Console.WriteLine(String.Format("タイトルは[{0}]です。", view.GetTitle()))
        Next
    End Sub
 
    ''' <summary>
    ''' エントリポイント
    ''' </summary>
    Public Shared Sub Main()
        Dim program As New Program()
        program.Run()
    End Sub
End Class
 
''' <summary>
''' IViewインターフェース
''' </summary>
Public Interface IView
    Function GetTitle() As String
End Interface
VB ViewParts.VBプロジェクト(C:\Parts.VBに出力している)
Imports System
Imports System.ComponentModel.Composition
Imports System.ComponentModel.Composition.Hosting
Imports DirectoryCatalogSample.VB
 
''' <summary>
''' メインビュー
''' </summary>
<Export(GetType(IView))> _
Public Class MainView
    Implements IView
    Public Function GetTitle() As String Implements DirectoryCatalogSample.VB.IView.GetTitle
        Return "Main Window"
    End Function
End Class
 
''' <summary>
''' メッセージダイアログ
''' </summary>
<Export(GetType(IView))> _
Public Class MessageDialog
    Implements IView
    Public Function GetTitle() As String Implements DirectoryCatalogSample.VB.IView.GetTitle
        Return "Message Dialog"
    End Function
End Class

サンプルコードを実行すると以下のように出力されます。

directorycatalog_details_output

 

サンプルコードでは、実行アプリケーションとして「DirectoryCatalogSample.Charp / DirectoryCatalogSample.VB」プロジェクトを作成し、エクスポートインターフェースのIViewインターフェースを定義しています。又、パーツ群を定義した「ViewParts.CSharp / ViewParts.VB」プロジェクトを別途作成し、コンパイル後の出力先を「C:\Parts.Charp / C:\Parts.VB」に出力しています。DirectoryCatalogクラスはコンストラクタで渡されたパスを元にアセンブリを検索し、見つかったアセンブリ内より指定されたパーツを検索しにいきます。コンテナーはDirectoryCatalogクラスより検索されたパーツを取得し、構築して提供します。DirectoryCatalogクラスを使用すると、アプリケーションの実行ディレクトリ以外の場所で配置されているアセンブリを動的に読む事が出来ます。この機構を使用する事で、アプリケーション実行時であっても動的にパーツを取得し使用したりする事が出来ます。DirectoryCatalogクラスはとても使い勝手が良くお気に入りのCatalogクラスです。是非動的なアセンブリの取得をMEFで試してみては如何でしょうか。

 

Source code

Managed Extensibility Framework(MEF)入門 -AssemblyCatalog-

MEFではコンテナーにある領域のパーツを取り込む為の機構として「カタログ」と言う物が用意されています。今回は「アセンブリ内のパーツを取り込む」為のカタログ『System.ComponentModel.Composition.Hosting.AssemblyCatalog』クラスを取り上げます。AssemblyCatalogクラスはMSDNでこのように説明されています。


AssemblyCatalog は、指定されたアセンブリ内にあるすべてのパーツを解析するために使用されます。ターゲット アセンブリは、オブジェクト参照またはパスによって示すことができます


AssemblyCatalogはコンストラクタで指定されたパス、又はオブジェクト参照を元に指定アセンブリよりパーツを検出します。そのAssemblyCatalogクラスをコンテナーのコンストラクタ引数に設定するとコンテナーはカタログよりパーツを取得しコンテナー内に構成します。

 

assemblycatalog_details

 

簡単なサンプルコードを元に見てみましょう。

C#

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
 
namespace AssemblyCatalogSample.CSharp
{
   public class Program
   {
      /// <summary>
      /// コンテナ
      /// </summary>
      private CompositionContainer container;
 
      /// <summary>
      /// View群
      /// </summary>
      [ImportMany()]
      public IEnumerable<IView> Views { get; set; }
 
      /// <summary>
      /// コンストラク
      /// </summary>
      private Program()
      {
         //Programクラスが格納されているアセンブリよりパーツを検出する。
         AssemblyCatalog catalog = new AssemblyCatalog(typeof(Program).Assembly);
         //指定カタログを用いてコンテナがパーツを検出する。
         this.container = new CompositionContainer(catalog);
      }
 
      /// <summary>
      /// 処理実行
      /// </summary>
      private void Run()
      {
         //コンテナより直接サービスを取得する。
         IEnumerable<IView> views = this.container.GetExportedValues<IView>();
         foreach (IView view in views)
         {
            Console.WriteLine(string.Format("タイトルは[{0}]です。", view.GetTitle()));
         }
 
         //自分のインスタンスにサービスをインポートして貰う。
         this.container.ComposeParts(this);
         foreach (IView view in this.Views)
         {
            Console.WriteLine(string.Format("タイトルは[{0}]です。", view.GetTitle()));
         }
      }
 
      /// <summary>
      /// エントリポイント
      /// </summary>
      public static void Main()
      {
         Program program = new Program();
         program.Run();
      }
   }
 
   /// <summary>
   /// IViewインターフェース
   /// </summary>
   public interface IView
   {
      /// <summary>
      /// タイトル取得
      /// </summary>
      string GetTitle();
   }
 
   /// <summary>
   /// メインビュー
   /// </summary>
   [Export(typeof(IView))]
   public class MainView : IView
   {
      public string GetTitle()
      {
         return "Main Window";
      }
   }
 
   /// <summary>
   /// メッセージダイアログ
   /// </summary>
   [Export(typeof(IView))]
   public class MessageDialog : IView
   {
      public string GetTitle()
      {
         return "Message Dialog";
      }
   }
}
VB

Imports System
Imports System.ComponentModel.Composition
Imports System.ComponentModel.Composition.Hosting
 
Public Class Program
   ''' <summary>
   ''' コンテナー
   ''' </summary>
   Private container As CompositionContainer
 
   ''' <summary>
   ''' View群
   ''' </summary>
   <ImportMany()> _
   Public Property Views() As IEnumerable(Of IView)
 
   ''' <summary>
   ''' エントリポイント
   ''' </summary>
   Public Shared Sub Main()
      Dim program As New Program()
      program.Run()
   End Sub
 
   ''' <summary>
   ''' コンストラク
   ''' </summary>
   Private Sub New()
      'Programクラスが格納されているアセンブリよりパーツを検出する。
      Dim catalog As New AssemblyCatalog(GetType(Program).Assembly)
      '指定カタログを用いてコンテナがパーツを検出する。
      Me.container = New CompositionContainer(catalog)
   End Sub
 
   ''' <summary>
   ''' 処理実行
   ''' </summary>
   ''' <remarks></remarks>
   Private Sub Run()
      'コンテナより直接サービスを取得する。
      Dim views As IEnumerable(Of IView) = Me.container.GetExportedValues(Of IView)()
      For Each view As IView In views
         Console.WriteLine(String.Format("タイトルは[{0}]です。", view.GetTitle()))
      Next
 
      '自分のインスタンスにサービスをインポートして貰う。
      Me.container.ComposeParts(Me)
      For Each view As IView In Me.Views
         Console.WriteLine(String.Format("タイトルは[{0}]です。", view.GetTitle()))
      Next
   End Sub
End Class
 
''' <summary>
''' IViewインターフェース
''' </summary>
Public Interface IView
   ''' <summary>
   ''' タイトル取得
   ''' </summary>
   Function GetTitle() As String
End Interface
 
''' <summary>
''' メインビュー
''' </summary>
<Export(GetType(IView))> _
Public Class MainView
   Implements IView
   ''' <summary>
   ''' タイトル取得
   ''' </summary>
   Public Function GetTitle() As String Implements IView.GetTitle
      Return "Main View"
   End Function
End Class
 
''' <summary>
''' メッセージダイアログ
''' </summary>
<Export(GetType(IView))> _
Public Class MessageDialog
   Implements IView
   ''' <summary>
   ''' タイトル取得
   ''' </summary>
   Public Function GetTitle() As String Implements IView.GetTitle
      Return "Message Dialog"
   End Function
End Class

サンプルコードを実行すると以下のように出力されます。

 

assemblycatalog_details_output

 

サンプルコードでは、Programクラスが格納されているアセンブリ内のパーツ群をAssemblyCatalogクラスを使用してコンテナーに取り込んでいます。今まで取り上げてきた記事では『System.ComponentModel.Composition.Hosting.CompositionBatch』クラスを使用してパーツをCompositionBatchクラスに追加していました。それではアプリケーションで使用するパーツ群を構成する為には一つずつ追加しなけらばならず、効果的とは言えません。カタログを使用する事によってある領域のパーツを取り込む事が出来ます。サンプルコードから分かるようにAssemblyCatalogクラスを使用すると、指定したアセンブリからパーツ群を検出して取り込む事が出来ます。MEFでは一般的にカタログを使用してパーツをコンテナーに取り込みます。「System.ComponentModel.Composition.Hosting名前空間には様々なカタログが用意されていますので、用途に合わせてカタログを使い柔軟なパーツ構成を行えます。是非試してみては如何でしょうか。

 

Source code

Managed Extensibility Framework(MEF)入門 -Custom Export-

MEFではエクスポート時にSystem.ComponentModel.Composition.ExportMetadataAttributeクラスを使用してメタデータの設定出来ます。メタデータ設定ではExportAttributeとExportMetadataAttributeを併用して実装する必要がありました。今回取り上げるカスタムエクスポートを作成するとエクスポート定義にメタデータ定義を実装する事が出来ます。

 

customexport_details

 

カスタムエクスポートクラスを作成するには、『System.ComponentModel.Composition.ExportAttribute』クラスを継承して作成します。また、カスタムエクスポートを使用すると『[MEF]Managed Extensibility Framework(MEF)入門 -ExportMetadata-』の記事で取り上げたいくつかの問題を解消する事が出来ます。それは以降の説明にて取り上げていきます。まずはサンプルコードから見てみましょう。

C#
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel.Composition;
    4 using System.ComponentModel.Composition.Hosting;
    5 
    6 namespace CustomExport.CSharp
    7 {
    8    public class Program
    9    {
   10       /// <summary>
   11       /// コンテナー
   12       /// </summary>
   13       private CompositionContainer container;
   14 
   15       /// <summary>
   16       /// コンストラク
   17       /// </summary>
   18       private Program()
   19       {
   20          this.container = new CompositionContainer();
   21          CompositionBatch batch = new CompositionBatch();
   22          batch.AddPart(new MainWindow());
   23          batch.AddPart(new ErrorDialog());
   24          this.container.Compose(batch);
   25       }
   26 
   27       /// <summary>
   28       /// 実行処理  
   29       /// </summary>
   30       private void Run()
   31       {
   32          IEnumerable<Lazy<IView, IMetadataView>> views = this.container.GetExports<IView, IMetadataView>();
   33          foreach (Lazy<IView, IMetadataView> view in views)
   34          {
   35             //WindowView型のみ出力
   36             if (view.Metadata.ViewType == ViewType.WindowView)
   37             {
   38                Console.WriteLine(string.Format("WindowViewのタイトルは{0}です。", view.Value.GetTitle()));
   39             }
   40          }
   41       }
   42 
   43       /// <summary>
   44       /// エントリポイント
   45       /// </summary>
   46       public static void Main()
   47       {
   48          Program program = new Program();
   49          program.Run();
   50       }
   51    }
   52 
   53    /// <summary>
   54    /// メインウィンドウ
   55    /// </summary>
   56    [ViewExport(ViewType = ViewType.WindowView)]
   57    public class MainWindow : IView
   58    {
   59       public string GetTitle()
   60       {
   61          return "メインウィンドウ";
   62       }
   63    }
   64 
   65    /// <summary>
   66    /// エラーダイアログ
   67    /// </summary>
   68    [ViewExport(ViewType = ViewType.DialogView)]
   69    public class ErrorDialog : IView
   70    {
   71       /// <summary>
   72       /// タイトル取得  
   73       /// </summary>
   74       public string GetTitle()
   75       {
   76          return "エラーダイアログ";
   77       }
   78    }
   79 
   80    /// <summary>
   81    /// エクスポート用インターフェース
   82    /// </summary>
   83    public interface IView
   84    {
   85       /// <summary>
   86       /// タイトル取得  
   87       /// </summary>
   88       string GetTitle();
   89    }
   90 
   91    /// <summary>
   92    /// メタデータビュー
   93    /// </summary>
   94    public interface IMetadataView
   95    {
   96       ViewType ViewType { get; }
   97    }
   98 
   99    /// <summary>
  100    /// 属性値識別用列挙体
  101    /// </summary>
  102    public enum ViewType
  103    {
  104       WindowView,
  105       DialogView
  106    }
  107 
  108    /// <summary>
  109    /// カスタムエクスポート属性クラス
  110    /// </summary>
  111    [MetadataAttribute]
  112    [AttributeUsage(AttributeTargets.Class)]
  113    public class ViewExportAttribute : ExportAttribute
  114    {
  115       /// <summary>
  116       /// コンストラク
  117       /// </summary>
  118       /// <remarks>コントラクト型でコントラクトを登録します。</remarks>
  119       public ViewExportAttribute()
  120          : this(string.Empty)
  121       { }
  122 
  123       /// <summary>
  124       /// コンストラク
  125       /// </summary>
  126       /// <param name="contractName">コントラクト名</param>
  127       /// <remarks>コントラクト名とコントラクト型でコントラクトを登録します。</remarks>
  128       public ViewExportAttribute(string contractName)
  129          : base(contractName, typeof(IView))
  130       { }
  131 
  132       /// <summary>
  133       /// メタデータ
  134       /// </summary>
  135       public ViewType ViewType { get; set; }
  136    }
  137 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    ''' <summary>
    7    ''' コンテナー
    8    ''' </summary>
    9    Private container As CompositionContainer
   10 
   11    ''' <summary>
   12    ''' コンストラク
   13    ''' </summary>
   14    Private Sub New()
   15       Me.container = New CompositionContainer()
   16       Dim batch As New CompositionBatch()
   17       batch.AddPart(New MainWindow())
   18       batch.AddPart(New ErrorDialog())
   19       Me.container.Compose(batch)
   20    End Sub
   21 
   22    ''' <summary>
   23    ''' 実行処理  
   24    ''' </summary>
   25    Private Sub Run()
   26       Dim views As IEnumerable(Of Lazy(Of IView, IMetadataView)) = Me.container.GetExports(Of IView, IMetadataView)()
   27       For Each view As Lazy(Of IView, IMetadataView) In views
   28          'WindowView型のみ出力
   29          If view.Metadata.ViewType = ViewType.WindowView Then
   30             Console.WriteLine(String.Format("WindowViewのタイトルは{0}です。", view.Value.GetTitle()))
   31          End If
   32       Next
   33    End Sub
   34 
   35    ''' <summary>
   36    ''' エントリポイント
   37    ''' </summary>
   38    Public Shared Sub Main()
   39       Dim program As New Program()
   40       program.Run()
   41    End Sub
   42 End Class
   43 
   44 ''' <summary>
   45 ''' メインウィンドウ
   46 ''' </summary>
   47 <ViewExport(ViewType:=ViewType.WindowView)> _
   48 Public Class MainWindow
   49    Implements IView
   50    ''' <summary>
   51    ''' タイトル取得  
   52    ''' </summary>
   53    Public Function GetTitle() As String Implements IView.GetTitle
   54       Return "メインウィンドウ"
   55    End Function
   56 End Class
   57 
   58 ''' <summary>
   59 ''' エラーダイアログ
   60 ''' </summary>
   61 <ViewExport(ViewType:=ViewType.DialogView)> _
   62 Public Class ErrorDialog
   63    Implements IView
   64    ''' <summary>
   65    ''' タイトル取得  
   66    ''' </summary>
   67    Public Function GetTitle() As String Implements IView.GetTitle
   68       Return "エラーダイアログ"
   69    End Function
   70 End Class
   71 
   72 ''' <summary>
   73 ''' エクスポート用インターフェース
   74 ''' </summary>
   75 Public Interface IView
   76    ''' <summary>
   77    ''' タイトル取得  
   78    ''' </summary>
   79    Function GetTitle() As String
   80 End Interface
   81 
   82 ''' <summary>
   83 ''' メタデータビュー
   84 ''' </summary>
   85 Public Interface IMetadataView
   86    ReadOnly Property ViewType() As ViewType
   87 End Interface
   88 
   89 ''' <summary>
   90 ''' 属性値識別用列挙体
   91 ''' </summary>
   92 Public Enum ViewType
   93    WindowView
   94    DialogView
   95 End Enum
   96 
   97 ''' <summary>
   98 ''' カスタムエクスポート属性クラス
   99 ''' </summary>
  100 <MetadataAttribute()> _
  101 <AttributeUsage(AttributeTargets.Class)> _
  102 Public Class ViewExportAttribute
  103    Inherits ExportAttribute
  104    ''' <summary>
  105    ''' コンストラク
  106    ''' </summary>
  107    ''' <remarks>コントラクト型でコントラクトを登録します。</remarks>
  108    Public Sub New()
  109       Me.New(String.Empty)
  110    End Sub
  111 
  112    ''' <summary>
  113    ''' コンストラク
  114    ''' </summary>
  115    ''' <param name="contractName">コントラクト名</param>
  116    ''' <remarks>コントラクト名とコントラクト型でコントラクトを登録します。</remarks>
  117    Public Sub New(ByVal contractName As String)
  118       MyBase.New(contractName, GetType(IView))
  119    End Sub
  120 
  121    ''' <summary>
  122    ''' メタデータ
  123    ''' </summary>
  124    Public Property ViewType() As ViewType
  125 End Class

上記サンプルコードの実行結果は以下の通りです。

 

customexport_details_output

 

上記ではコントラクトがIViewインターフェース型のExportAttributeクラスを継承したカスタムエクスポート属性クラスのViewExportAttributeを作成しています。独自の属性クラスを作成する場合は、クラス名のサフィックス(接尾語)に『Attribute』を付けるルールがあります。又、上記サンプルではViewExportAttributeクラスに2つの属性を定義しています。

1つめの『System.ComponentModel.Composition.MetadataAttributeAttribute』クラスを属性定義すると、ExporAttributeクラス(今回はExportAttributeクラスを継承しているのでViewExportAttributeクラスが)で定義されたプロパティ、フィールド、メソッドをメタデータとして提供する事が可能となります。よってViewExportAttributeクラスで定義されているViewTypeプロパティがメタデータとして認識されます。

2つめの『System.AttributeUsageAttribute』クラスを属性として定義すると、定義したクラスの使用方法を制御する事が出来ます。AttributeUsageAttributeクラスのコンストラクタ引数に『System.AttributeTargets 』列挙体を用いて属性として定義出来るスコープを設定します。ViewExportAttributeクラスでは「AttributeTargets.Class」が設定されているのでクラスにのみ属性として定義が可能なクラスになります。

サンプルコードのViewExportAttributeクラスではコンストラクタを2つ定義し、1つはコントラクト名を設定出来るようにしてあります。これによってコントラクトとして内包されているIViewインターフェース型とコントラクト名を設定する事が可能になります。ExportAttributeクラスを使用した場合と比べてカスタムエクスポート属性クラスを作成した場合はエクスポート定義が明確になり、かつメタデータも同時に設定出来るようになります。出力結果からカスタムエキスポート属性クラスで定義されているコントラクトとメタデータが認識されている事が分かります。またメタデータビューも正しく取得されている事が分かります。ただ、上記サンプルでは1点問題があります。



カスタムエクスポート属性クラスのメタデータ(ViewType)とメタデータビューインターフェースのメタデータ(ViewType)を同じにし管理しなければならない。


例えばメタデータビューのメタデータ(ViewType)の名前をViewTypesと変更した場合、出力結果は以下の通りになります。

 

customexport_details_output3

 

メタデータビューのメタデータ名を変更した事によって、カスタムエクスポート属性クラスのメタデータと関連性が損なわれてしまった為に正しくメタデータを認識出来なくなってしまいました。又、この関連性の欠損はコンパイル時でも発見する事は出来ず、原因特定するのが困難な状況を生み出してしまいます。この問題は、メタデータビューとカスタムエクスポート属性クラスのメタデータをインターフェース制約を使用して解決する事が出来ます。そのサンプルコードは以下になります。

C#
 /// <summary>
 /// メタデータビュー
 /// </summary>
 public interface IMetadataView
{
    [DefaultValue(typeof(ViewType), "WindowView")]
    ViewType ViewType { get; }
}
 
 /// <summary>
 /// カスタムエクスポート属性クラス
 /// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
 public class ViewExportAttribute : ExportAttribute, IMetadataView
{
    /// <summary>
    /// コンストラク
    /// </summary>
    /// <remarks>コントラクト型でコントラクトを登録します。</remarks>
    public ViewExportAttribute()
       : this(string.Empty)
    { }
 
    /// <summary>
    /// コンストラク
    /// </summary>
    /// <param name="contractName">コントラクト名</param>
    /// <remarks>コントラクト名とコントラクト型でコントラクトを登録します。</remarks>
    public ViewExportAttribute(string contractName)
       : base(contractName, typeof(IView))
    { }
 
    /// <summary>
    /// メタデータ
    /// </summary>
    public ViewType ViewType { get; set; }
    }
}
VB
 ''' <summary>
 ''' メタデータビュー
 ''' </summary>
 Public Interface IMetadataView
    ReadOnly Property ViewType() As ViewType
 End Interface
 
 ''' <summary>
 ''' メインウィンドウ
 ''' </summary>
<ViewExport(ViewType.WindowView)> _
 Public Class MainWindow
    Implements IView
    ''' <summary>
    ''' タイトル取得
    ''' </summary>
    Public Function GetTitle() As String Implements IView.GetTitle
       Return "メインウィンドウ"
    End Function
 End Class
 
 ''' <summary>
 ''' カスタムエクスポート属性クラス
 ''' </summary>
<MetadataAttribute()> _
<AttributeUsage(AttributeTargets.Class)> _
 Public Class ViewExportAttribute
    Inherits ExportAttribute
    Implements IMetadataView
 
    ''' <summary>
    ''' ViewTypeフィールド
    ''' </summary>
    Private _viewType As ViewType
 
    ''' <summary>
    ''' コンストラク
    ''' </summary>
    ''' <remarks>コントラクト型でコントラクトを登録します。</remarks>
    Public Sub New(Optional ByVal viewType As ViewType = ViewType.WindowView)
       Me.New(String.Empty, viewType)
    End Sub
 
    ''' <summary>
    ''' コンストラク
    ''' </summary>
    ''' <param name="contractName">コントラクト名</param>
    ''' <remarks>コントラクト名とコントラクト型でコントラクトを登録します。</remarks>
    Public Sub New(ByVal contractName As String, Optional ByVal viewType As ViewType = ViewType.WindowView)
       MyBase.New(contractName, GetType(IView))
       Me._viewType = viewType
    End Sub
 
    ''' <summary>
    ''' メタデータ
    ''' </summary>
    Public ReadOnly Property ViewType As ViewType Implements IMetadataView.ViewType
       Get
          Return _viewType
       End Get
    End Property
 End Class

上記ではカスタムエクスポート属性クラス(ViewExportAttribute)にメタデータビューインターフェース(IMetadataView)を継承させています。これによってメタデータビューで定義しているメタデータをインターフェース制約によって強制する事が出来ます。これによってどちらかのメタデータが変更された場合はコンパイル時にエラーとして検出出来るようになります。サンプルコードではC#VBの言語仕様の違いで実装が一部異なっている所があります。

C#ではインターフェースで定義されている読み取り専用プロパティを書き込み可能なプロパティとして継承可能な為、カスタムエクスポート属性クラスにインターフェースを継承させるだけで済みます。又、メタデータを省略された場合を想定してメタデータビューのメタデータに『System.ComponentModel.DefaultValueAttribute』クラスを属性として定義し既定値を設定します。

VBではインターフェースで定義されている読み取り専用プロパティは読み取り専用プロパティとして継承しなければならない為、カスタムエクスポート属性クラスにインターフェースを継承させ読み取り専用メタデータの値をOptionalキーワードを使用してコンストラクタ引数で設定するようにします。これによってVBでは既定値をDefaultValueAttributeクラスを使用せずに設定できます。またエクスポート定義もコンストラクタ引数を使用するように変更します。

このようにしてメタデータとカスタムエクスポート属性クラスのメタデータの関連付けをする事で見つけずらいバグを防ぐ事が出来ます。カスタムエクスポート属性クラスを作成する事で、明確でかつ修正変更に対応出来るエクスポートが実装出来る事がご理解頂けたかと思います。是非カスタムエクスポート属性クラスを活用してみては如何でしょうか。

 

Source code

Managed Extensibility Framework(MEF)入門 -ExportMetadata-

MEFではエクスポートするサービスに『メタデータ(エクスポート属性)』を付ける事が出来ます。これを使用する事によって、同一コントラクトのサービス群を区別する事が出来ます。主にImportMany属性を使用して取り込んだ同一のサービス群やコンテナーより取得した同一コントラクトのサービス群に使用します。メタデータを識別する為に『Lazy<T, TMetadata>・Lazy(Of T, TMetadata)』クラスを使用します。遅延エクスポートで使用する「Lazy<T>・Lazy(Of T)」クラスを継承したクラスであり、遅延エクスポートの恩恵も得ます。サービス群を区別する際にサービスインスタンスを都度生成していては効率が良くなく、サービスの区別後にインスタンス生成を行うのが望ましい事から「Lazy<T>・Lazy(Of T)」クラスを拡張して作成されてます。

 

exportmetadata_details

 

このようにサービスにメタデータとして「属性名(キー)、属性値(値)」を設定する事が出来ます。これには『System.ComponentModel.Composition.ExportMetadataAttribute』クラスを使用し、エクスポートするサービスに属性として定義します。サービスにはメタデータを複数設定する事も出来ます。それではサンプルコードを元に見ていきましょう。

 

基本実装

C#
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.ComponentModel.Composition;
    5 using System.ComponentModel.Composition.Hosting;
    6 
    7 namespace ExportMetadata.CSharp
    8 {
    9    public class Program
   10    {
   11       private CompositionContainer container;
   12 
   13       private Program()
   14       {
   15          container = new CompositionContainer();
   16          CompositionBatch batch = new CompositionBatch();
   17          batch.AddPart(new MessageSender());
   18          container.Compose(batch);
   19       }
   20 
   21       private void Run()
   22       {
   23          //Metadataは既定ではIDictionary<string, object>で取得出来る。
   24          Lazy<IMessageSender, Dictionary<string, object>> messageSender = this.container.GetExport<IMessageSender, Dictionary<string, object>>();
   25          //Lazy<T, TMetadata>のIMetadataプロパティからエクスポート属性値を取得する。
   26          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata["MetadataName"]));
   27          //メタデータにアクセスしている時はまだインスタンスは生成されていない。
   28          Console.WriteLine(string.Format("instance is created={0}", messageSender.IsValueCreated.ToString()));
   29          Console.WriteLine(messageSender.Value.GetMessage());
   30       }
   31 
   32       public static void Main(string[] args)
   33       {
   34          Program program = new Program();
   35          program.Run();
   36       }
   37    }
   38 
   39    public interface IMessageSender
   40    {
   41       string GetMessage();
   42    }
   43 
   44    //メタデータに属性名="MetadataName"、属性値="MetadataValue"を設定。
   45    [ExportMetadata("MetadataName", "MetadataValue")]
   46    [Export(typeof(IMessageSender))]
   47    public class MessageSender : IMessageSender
   48    {
   49       public string GetMessage()
   50       {
   51          return "メッセージです。";
   52       }
   53    }
   54 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    Private Sub New()
    9       Me.container = New CompositionContainer()
   10       Dim batch As New CompositionBatch()
   11       batch.AddPart(New MessageSender())
   12       Me.container.Compose(batch)
   13    End Sub
   14 
   15    Private Sub Run()
   16       'Metadataは既定ではIDictionary<string, object>で取得出来る。
   17       Dim messageSender As Lazy(Of IMessageSender, IDictionary(Of String, Object)) = Me.container.GetExport(Of IMessageSender, IDictionary(Of String, Object))()
   18       'Lazy(Of T, TMetadata)のIMetadataプロパティからエクスポート属性値を取得する。
   19       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata("MetadataName")))
   20       'メタデータにアクセスしている時はまだインスタンスは生成されていない。
   21       Console.WriteLine(String.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()))
   22       Console.WriteLine(messageSender.Value.GetMessage())
   23    End Sub
   24 
   25    Public Shared Sub Main()
   26       Dim program As New Program()
   27       program.Run()
   28    End Sub
   29 End Class
   30 
   31 Public Interface IMessageSender
   32    Function GetMessage() As String
   33 End Interface
   34 
   35 'メタデータに属性名="MetadataName"、属性値="MetadataValue"を設定。
   36 <ExportMetadata("MetadataName", "MetadataValue")> _
   37 <Export(GetType(IMessageSender))> _
   38 Public Class MessageSender
   39    Implements IMessageSender
   40    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   41       Return "メッセージです。"
   42    End Function
   43 End Class

上記サンプルコードの出力結果は以下の通りになります。

 

exportmetadata_details_output

 

サンプルコードではMessageSenderクラスをサービスとしてエクスポートしており、ExportMetadataAttributeクラスを使用して「属性名=”MetadataName”、属性値=”MetadataValue”」をサービスに付与しています。エクスポートされたサービスを使用する側ではLazy<T, TMetadata>・Lazy(Of T, TMetadata)クラスを使用してサービスを受け取り、『Metadataプロパティ』を通じてエクスポートされたサービスのメタデータを見ています。Lazy<T, TMetadata>・Lazy(Of T, TMetadata)クラスは遅延エクスポートが行えるのでValueプロパティにアクセスするまでサービスのインスタンスが生成されておりません。同一コントラクトのサービス群を区別している時にインスタンスが生成されない事は理にかなっており、区別後の必要なサービスのみインスタンスが作られるわけです。又、ExportMetadataAttributeのコンストラクタ引数に属性名と属性値が設定されていますが、ExportMetadataAttributeクラスのコンストラクタ引数は『属性名=string型、属性値=object型』となっています。よってTMetadataに「IDictionary<string, object>・IDictionary(Of String, Object)」が使用出来るのです。

 

メタデータビューの使用

エクスポートしたサービスにメタデータを設定する方法と取得する方法は上記サンプルで分かりました。しかし上記サンプルコードには幾つかの問題があります。



  1. メタデータの属性名がstring型である為、エクスポート側と使用する側で同一の文字列を管理する必要がある。
  2. メタデータの属性値がobject型である為、使用する側ではエクスポート側で設定された型を意識する必要がある。
  3. コンパイル時の検出が行えない。

ExportMetadataAttributeを使用した場合にメタデータへアクセス用する為にIDictionary<string, object>・IDictionary(Of String, Object)を使用していましたが、上記問題を抱えてしまう為に推奨されていません。これらの問題を『使用する側で解決する』為の機構として『メタデータビュー』と言う物があります。メタデータビューとは以下のように定義されています。



メタデータ ビューは、エクスポートされるメタデータのキーに対応する、読み取り専用のプロパティを定義するインターフェイスです。metadata プロパティにアクセスすると、MEF では TMetadata が動的に実装され、エクスポートから提供されたメタデータを基に値が設定されます。


初めのサンプルコードではIDictionary<string, object>・IDictionary(Of String, Object)を使用していた部分をメタデータビューに変える事が出来ます。「属性名と同じ名前のプロパティを持ったインターフェース」を作成しメタデータビューとして使います。このサンプルコードを見てみましょう。

C#
    1 using System;
    2 using System.Collections.Generic;
    3 using System.ComponentModel;
    4 using System.ComponentModel.Composition;
    5 using System.ComponentModel.Composition.Hosting;
    6 
    7 namespace ExportMetadataUsingIMetadataView.CSharp
    8 {
    9    public class Program
   10    {
   11       private CompositionContainer container;
   12 
   13       private Program()
   14       {
   15          container = new CompositionContainer();
   16          CompositionBatch batch = new CompositionBatch();
   17          batch.AddPart(new MessageSender());
   18          container.Compose(batch);
   19       }
   20 
   21       private void Run()
   22       {
   23          //IMetadataViewインターフェースをメタデータビューとして使用している。
   24          Lazy<IMessageSender, IMetadataView> messageSender = this.container.GetExport<IMessageSender, IMetadataView>();
   25          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata.FirstMetadataName));
   26          Console.WriteLine(string.Format("Value={0}", messageSender.Metadata.SecondMetadataName));
   27          Console.WriteLine(messageSender.Value.GetMessage());
   28       }
   29 
   30       static void Main(string[] args)
   31       {
   32          Program program = new Program();
   33          program.Run();
   34       }
   35    }
   36 
   37    /// <summary>
   38    /// メタデータビュー用インターフェース
   39    /// </summary>
   40    public interface IMetadataView
   41    {
   42       //プロパティ名が属性名に対応している。
   43       ViewTypes FirstMetadataName { get; }
   44       ViewTypes SecondMetadataName { get; }
   45    }
   46 
   47    public interface IMessageSender
   48    {
   49       string GetMessage();
   50    }
   51 
   52    [ExportMetadata(ViewMetadata.FirstMetadataName, ViewTypes.FirstValue)]
   53    [ExportMetadata(ViewMetadata.SecondMetadataName, ViewTypes.SecondValue)]
   54    [Export(typeof(IMessageSender))]
   55    public class MessageSender : IMessageSender
   56    {
   57       public string GetMessage()
   58       {
   59          return "メッセージです。";
   60       }
   61    }
   62 
   63    /// <summary>
   64    /// 属性名識別用クラス
   65    /// </summary>
   66    public class ViewMetadata
   67    {
   68       public const string FirstMetadataName = "FirstMetadataName";
   69       public const string SecondMetadataName = "SecondMetadataName";
   70    }
   71 
   72    /// <summary>
   73    /// 属性値識別用列挙体
   74    /// </summary>
   75    public enum ViewTypes
   76    {
   77       FirstValue,
   78       SecondValue
   79    }
   80 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    Private Sub New()
    9       Me.container = New CompositionContainer()
   10       Dim batch As New CompositionBatch()
   11       batch.AddPart(New MessageSender())
   12       Me.container.Compose(batch)
   13    End Sub
   14 
   15    Private Sub Run()
   16       'IMetadataViewインターフェースをメタデータビューとして使用している。
   17       Dim messageSender As Lazy(Of IMessageSender, IMetadataView) = Me.container.GetExport(Of IMessageSender, IMetadataView)()
   18       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata.FirstMetadataName))
   19       Console.WriteLine(String.Format("Value={0}", messageSender.Metadata.SecondMetadataName))
   20       Console.WriteLine(messageSender.Value.GetMessage())
   21    End Sub
   22 
   23    Public Shared Sub Main()
   24       Dim program As New Program()
   25       program.Run()
   26    End Sub
   27 End Class
   28 
   29 ''' <summary>
   30 ''' メタデータビュー用インターフェース
   31 ''' </summary>
   32 Public Interface IMetadataView
   33    'プロパティ名が属性名に対応している
   34    ReadOnly Property FirstMetadataName As ViewTypes
   35    ReadOnly Property SecondMetadataName As ViewTypes
   36 End Interface
   37 
   38 Public Interface IMessageSender
   39    Function GetMessage() As String
   40 End Interface
   41 
   42 <ExportMetadata(ViewMetadata.FirstMetadataName, ViewTypes.FirstValue)> _
   43 <ExportMetadata(ViewMetadata.SecondMetadataName, ViewTypes.SecondValue)> _
   44 <Export(GetType(IMessageSender))> _
   45 Public Class MessageSender
   46    Implements IMessageSender
   47    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   48       Return "メッセージです。"
   49    End Function
   50 End Class
   51 
   52 ''' <summary>
   53 ''' 属性名識別用クラス
   54 ''' </summary>
   55 Public Class ViewMetadata
   56    Public Const FirstMetadataName As String = "FirstMetadataName"
   57    Public Const SecondMetadataName As String = "SecondMetadataName"
   58 End Class
   59 
   60 ''' <summary>
   61 ''' 属性値識別用列挙体
   62 ''' </summary>
   63 Public Enum ViewTypes
   64    FirstValue
   65    SecondValue
   66 End Enum

これを実行すると以下のように出力されます。

 

exportmetadata_details_output2

 

上記ではExportMetadataAttributeクラスに指定する属性名をリテラル文字ではなく定数を使用して指定しています。これにより同じ値を持った複数の属性名を一度に変更する事が出来ます。又、属性値も列挙体を用いて指定する値を限定させています。メタデータビューとしてIMetadataViewインターフェースを作成しました。このインターフェースを定義する場合には以下のルールがあります。



  • 読み取り専用のプロパティである
  • プロパティ名は属性名と同じ名前である
  • プロパティ型は属性値と同じ型である

サンプルでは上記ルールに沿ったプロパティを2つ実装させています。このルールは必ず守る必要があります。ここでどれか規則を守っていない物が存在すると、サービスの取得時に例外が発生します。ルールに基づいたメタデータビューを作成したあと、Lazy<T, TMetadata>・Lazy(Of T, TMetadata)のTMetadataに指定する事でMetadataプロパティにアクセスした際に動的にMetadataインスタンスを作成してくれます。これは遅延エクスポートと同じ機構です。メタデータビューを使用する事で、使用する側へのタイプセーフな機構を提供する事が出来ます。

メタデータビューを使用する事で幾つかの問題は解消されましたが、未だまだ問題を抱えています。それは以下の通りです。



  • エクスポートのメタデータの属性名を明示的に都度定義する必要がある。

上記問題はExportMetadataAttributeのコンストラクタ引数がstring型の属性名、object型の属性値と限定されている事によって発生しています。これを解決する為にはExportAttributeを継承したカスタムエクスポートクラスを作成する必要があります。このカスタムエクスポートクラスについては別途取り上げたいと思います。

 

Source Code

Managed Extensibility Framework(MEF)入門 -Lazy(遅延エクスポート)-

MEFではサービスのエクスポートを遅延させる事が出来ます。コンテナーに登録されたサービスを取得する場合、コントラクトと取得型を指定して希望のサービスを取得するか、コンテナーよりパーツにサービスをインポートしてもらいサービスを使用するのが一般的です。ですが、取得対象のサービスのインスタンスが大きかったりインポートされるサービス数が膨大にある場合はメモリーに圧迫をかけてしまう事になります。こういった場合に「遅延エクスポート」という機構を使用する事によって、実際にサービスを使用する場合にのみインスタンスの生成を行わせる事が出来ます。遅延エクスポートを使用する場合には『System.Lazy<T>・System.Lazy(Of T)』クラスを使用します。

 

lazy_details

 

遅延エクスポートを使用するには、図のようにエクスポートされたサービスをインポート時、又はコンテナーより取得する際にLazyクラスに内包する形で取得します。Lazyクラスの『Valueプロパティ』を通じてサービスを取得します。LazyクラスはValueプロパティにアクセスされるまでサービスのインスタンスの生成を遅延させます。Valueプロパティにサービスインスタンスが生成されているか判別するプロパティの『IsValueCreatedプロパティ』も用意されています。これをサンプルコードを元に見てみましょう。

C#
    1 using System;
    2 using System.ComponentModel.Composition;
    3 using System.ComponentModel.Composition.Hosting;
    4 
    5 namespace LazyExport.CSharp
    6 {
    7    public class Program
    8    {
    9       private CompositionContainer container;
   10 
   11       [Import]
   12       public Lazy<IMessageSender> MessageSender { get; set; }
   13 
   14       private Program()
   15       {
   16          this.container = new CompositionContainer();
   17          CompositionBatch batch = new CompositionBatch();
   18          batch.AddPart(new MessageSender());
   19          batch.AddPart(this);
   20          this.container.Compose(batch);
   21       }
   22 
   23       private void Run()
   24       {
   25          //コンテナーより直接取得したLazyクラスを使用。
   26          Lazy<IMessageSender> messageSender = this.container.GetExport<IMessageSender>();
   27          Console.WriteLine(string.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()));
   28          Console.WriteLine(messageSender.Value.GetMessage());
   29          Console.WriteLine(string.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()));
   30 
   31          Console.WriteLine();
   32 
   33          //インポートされたLazyクラスを使用。
   34          Console.WriteLine(string.Format("instance is created = {0}", this.MessageSender.IsValueCreated.ToString()));
   35          Console.WriteLine(this.MessageSender.Value.GetMessage());
   36          Console.WriteLine(string.Format("instance is created = {0}", this.MessageSender.IsValueCreated.ToString()));
   37       }
   38 
   39       public static void Main(string[] args)
   40       {
   41          Program program = new Program();
   42          program.Run();
   43       }
   44    }
   45 
   46    public interface IMessageSender
   47    {
   48       string GetMessage();
   49    }
   50 
   51    [Export(typeof(IMessageSender))]
   52    public class MessageSender : IMessageSender
   53    {
   54       public string GetMessage()
   55       {
   56          return "メッセージです。";
   57       }
   58    }
   59 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    <Import()> _
    9    Public Property MessageSender As Lazy(Of IMessageSender)
   10 
   11    Private Sub New()
   12       Me.container = New CompositionContainer()
   13       Dim batch As New CompositionBatch()
   14       batch.AddPart(New MessageSender())
   15       batch.AddPart(Me)
   16       Me.container.Compose(batch)
   17    End Sub
   18 
   19    Private Sub Run()
   20       'コンテナーより直接取得したLazyクラスを使用。
   21       Dim messageSender As Lazy(Of IMessageSender) = Me.container.GetExport(Of IMessageSender)()
   22       Console.WriteLine(String.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()))
   23       Console.WriteLine(messageSender.Value.GetMessage())
   24       Console.WriteLine(String.Format("instance is created = {0}", messageSender.IsValueCreated.ToString()))
   25 
   26       Console.WriteLine()
   27 
   28       'インポートされたLazyクラスを使用。
   29       Console.WriteLine(String.Format("instance is created = {0}", Me.MessageSender.IsValueCreated.ToString()))
   30       Console.WriteLine(Me.MessageSender.Value.GetMessage())
   31       Console.WriteLine(String.Format("instance is created = {0}", Me.MessageSender.IsValueCreated.ToString()))
   32    End Sub
   33 
   34    Public Shared Sub Main()
   35       Dim program As New Program()
   36       program.Run()
   37    End Sub
   38 End Class
   39 
   40 Public Interface IMessageSender
   41    Function GetMessage() As String
   42 End Interface
   43 
   44 <Export(GetType(IMessageSender))> _
   45 Public Class MessageSender
   46    Implements IMessageSender
   47    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   48       Return "メッセージです。"
   49    End Function
   50 End Class

上記サンプルコードを実行すると以下のような結果が出力されます。

 

lazy_details_output

 

出力結果から分かるとおりにLazyインスタンスValueプロパティにアクセスするまでインスタンスが存在していない事が分かります。ValueプロパティにアクセスされるとLazyクラスはサービスのインスタンスを生成します。このようにエクスポートされたサービスを遅延させる事でコンテナーでエクスポートされたサービスのインスタンスを常にメモリに持っておく必要が無くなるのです。サービスインスタンスが大きい場合は即時使用されないサービス等がある場合はLazyクラスを上手く利用すると良いでしょう。

 

Source code

Managed Extensibility Framework(MEF)入門 -Contract-

MEFでサービスのエクスポートやインポート時に識別子を付与する事が出来ます。この識別子を『コントラクト』と言います。コントラクトには以下の種類があります。



名前コントラクトは文字列で識別子を設定し、型コントラクトは型で識別子を設定します。各パーツはサービスをエクスポートしたりインポートする際にコントラクトを指定します。

 

contract_details

 

どのようにサービスに対してコントラクトを指定するかサンプルコードを元に見てみましょう。

 

エクスポートの既定のコントラクト

C#
   32 //既定では定義している型に依存する。
   33 //今回は何も指定していないのでMessageSender型で型コントラクトが指定される。
   34 [Export]
   35 public class MessageSender
   36 {
   37    public string GetMessage()
   38    {
   39       return "MessageSenderからのメッセージです。";
   40    }
   41 }
VB
   26 '既定では定義している型に依存する。
   27 '今回は何も指定していないのでMessageSender型で型コントラクトが指定される。
   28 <Export()> _
   29 Public Class MessageSender
   30    Public Function GetMessage() As String
   31       Return "MessageSenderからのメッセージです。"
   32    End Function
   33 End Class

上記ではMessageSenderクラスをサービスとしてエクスポートしている例です。ExportAttributeクラスは既定で定義された型を型コントラクトとして認識する為、サンプルコードではMessageSenderクラス型がコントラクトとして指定されている事になります。

 

エクスポートで型コントラクトの指定

C#
   32 public interface IMessageSender
   33 {
   34    string GetMessage();
   35 }
   36 
   37 //IMessageSenderインターフェース型をコントラクトに指定している。
   38 [Export(typeof(IMessageSender))]
   39 public class MessageSender : IMessageSender
   40 {
   41    public string GetMessage()
   42    {
   43       return "MessageSenderからのメッセージです。";
   44    }
   45 }
VB
   26 Public Interface IMessageSender
   27    Function GetMessage() As String
   28 End Interface
   29 
   30 'IMessageSenderインターフェース型をコントラクトに指定している。
   31 <Export(GetType(IMessageSender))> _
   32 Public Class MessageSender
   33    Implements IMessageSender
   34    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   35       Return "MessageSenderからのメッセージです。"
   36    End Function
   37 End Class

上記ではIMessageSenderインターフェース型のコントラクトとしてMessageSenderクラスをエクスポートしている例です。ExportAttributeクラスのコンストラクタで型を指定すると型コントラクトとしてコントラクトに指定されます。ここで注意が必要です。「型コントラクトで指定した型(IMessageSenderインターフェース)は、エクスポートするクラス(MessageSenderクラス)と互換関係である」必要があります。これはコンパイルで検出されないので十分注意して下さい。

 

エクスポートで名前コントラクトの指定

C#
   37 //"MessageSender"という名前をコントラクトに指定している。
   38 [Export("MessageSender")]
   39 public class MessageSender
   40 {
   41    public string GetMessage()
   42    {
   43       return "MessageSenderからのメッセージです。";
   44    }
   45 }
VB
   30 '"MessageSender"という名前をコントラクトに指定している。
   31 <Export("MessageSender")> _
   32 Public Class MessageSender
   33    Public Function GetMessage() As String
   34       Return "MessageSenderからのメッセージです。"
   35    End Function
   36 End Class

上記では”MessageSender”という名前でMessageSenderクラスをエクスポートしている例です。ExportAttributeクラスのコンストラクタで文字列を指定すると名前コントラクトとしてコントラクトに指定されます。

 

エクスポートで型コントラクトと名前コントラクタの指定

C#
   32 public interface IMessageSender
   33 {
   34    string GetMessage();
   35 }
   36 
   37 //名前コントラクトと型コントラクトを指定している。
   38 [Export("MessageSender", typeof(IMessageSender))]
   39 public class MessageSender : IMessageSender
   40 {
   41    public string GetMessage()
   42    {
   43       return "MessageSenderからのメッセージです。";
   44    }
   45 }
VB
   26 Public Interface IMessageSender
   27    Function GetMessage() As String
   28 End Interface
   29 
   30 '名前コントラクトと型コントラクトを指定している。
   31 <Export("MessageSender", GetType(IMessageSender))> _
   32 Public Class MessageSender
   33    Implements IMessageSender
   34    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   35       Return "MessageSenderからのメッセージです。"
   36    End Function
   37 End Class

上記では名前コントラクトと型コントラクトを同時に指定している例です。このようにエクスポートを定義している型と互換性のある型コントラクトと名前コントラクトを指定する事で、サービスの識別子を柔軟に定義する事が出来ます。

 

インポートでのコントラクト

上記サンプルコードではエクスポートのコントラクトを中心に書きましたが、インポートでもエクスポートと同様にコントラクトを指定する事が出来ます。インポートでコントラクトを指定する事で柔軟なサービスの提供を受ける事が出来ます。

C#
   11 //既定で定義される型コントラクトにてインポート。
   12 [Import]
   13 public MessageSender MessageSender { get; set; }
   14 
   15 //名前コントラクトにてインポート。
   16 [Import("MessageSender")]
   17 public IMessageSender MessageSender { get; set; }
   18 
   19 //型コントラクトにてインポート。
   20 [Import(typeof(IMessageSender))]
   21 public object MessageSender { get; set; }
   22 
   23 //名前コントラクトと型コントラクトにてインポート。
   24 [Import("MessageSender", typeof(IMessageSender))]
   25 public object MessageSender { get; set; }
VB
   55 '既定で定義される型コントラクトにてインポート。
   56 <Import()> _
   57 Public Property MessageSender() As MessageSender
   58 
   59 ' 名前コントラクトにてインポート。
   60 <Import("MessageSender")> _
   61 Public Property MessageSender() As IMessageSender
   62 
   63 '型コントラクトにてインポート。
   64 <Import(GetType(IMessageSender))> _
   65 Public Property MessageSender() As Object
   66 
   67 '名前コントラクトと型コントラクトにてインポート。
   68 <Import("MessageSender", GetType(IMessageSender))> _
   69 Public Property MessageSender() As Object

上記のようにエクスポートと同様にインポートでも名前コントラクトと型コントラクトが使用出来ます。インポート側ではコントラクトを指定してコンテナーよりインポートしてもらうサービスにフィルタをかける事が出来ます。ただし、型コントラクトは一般的にはサービスがエクスポートされる時点で抽象化される事が大半の為、インポート時に型コントラクトを明示的に指定して利用するケースは少ないかと思います。またちょっとしたトピックスになりますが、インポート時の型コントラクトは具象型を指定する事が出来てしまいます。例えば、IMessageSenderインターフェース型のプロパティに型コントラクトとして具象クラスのMessageSenderクラスを指定すると言った事が可能です。ですが、インターフェースと実装の分離を実現する機構で、そのメリットは余りないように思えます。

 

コントラクトを使用したサンプル

最後にコントラクトを使用したサンプルです。

C#
    1 using System;
    2 using System.ComponentModel.Composition;
    3 using System.ComponentModel.Composition.Hosting;
    4 using System.Collections.Generic;
    5 
    6 namespace Contract.CSharp
    7 {
    8    public class Program
    9    {
   10       private CompositionContainer container;
   11      
   12       [Import]
   13       public IMessageSender MessageSender { get; set; }
   14 
   15       [Import("Japanese")]
   16       public IMessageSender JapaneseMessageSender { get; set; }
   17 
   18       [Import("English")]
   19       public IMessageSender EnglishMessageSender { get; set; }
   20 
   21       private Program()
   22       {
   23          this.container = new CompositionContainer();
   24          CompositionBatch batch = new CompositionBatch();
   25          batch.AddPart(new MessageSender());
   26          batch.AddPart(new JapaneseMessageSender());
   27          batch.AddPart(new EnglishMessageSender());
   28          batch.AddPart(this);
   29          this.container.Compose(batch);
   30       }
   31 
   32       private void Run()
   33       {
   34          //IMessageSender型の取得  
   35          IMessageSender messageSender = this.container.GetExportedValue<IMessageSender>();
   36          Console.WriteLine(MessageSender.GetMessage());
   37 
   38          //JapaneseMessageSenderの取得  
   39          IMessageSender japaneseMessageSender = this.container.GetExportedValue<IMessageSender>("Japanese");
   40          Console.WriteLine(japaneseMessageSender.GetMessage());
   41 
   42          //EnglishMessageSenderの取得  
   43          IMessageSender englishMessageSender = this.container.GetExportedValue<IMessageSender>("English");
   44          Console.WriteLine(englishMessageSender.GetMessage());
   45 
   46          //インポートされたIMessageSender
   47          Console.WriteLine(this.MessageSender.GetMessage());
   48 
   49          //インポートされたJapaneseMessageSender
   50          Console.WriteLine(this.JapaneseMessageSender.GetMessage());
   51 
   52          //インポートされたEnglishMessageSender
   53          Console.WriteLine(this.EnglishMessageSender.GetMessage());
   54       }
   55 
   56       public static void Main(string[] args)
   57       {
   58          Program program = new Program();
   59          program.Run();
   60       }
   61    }
   62 
   63    public interface IMessageSender
   64    {
   65       string GetMessage();
   66    }
   67 
   68    [Export(typeof(IMessageSender))]
   69    public class MessageSender : IMessageSender
   70    {
   71       public string GetMessage()
   72       {
   73          return "MessageSenderからのメッセージです。";
   74       }
   75    }
   76 
   77    [Export("Japanese", typeof(IMessageSender))]
   78    public class JapaneseMessageSender : IMessageSender
   79    {
   80       public string GetMessage()
   81       {
   82          return "メッセージです。";
   83       }
   84    }
   85 
   86    [Export("English", typeof(IMessageSender))]
   87    public class EnglishMessageSender : IMessageSender
   88    {
   89       public string GetMessage()
   90       {
   91          return "It is a message.";
   92       }
   93    }
   94 }
VB
    1 Imports System
    2 Imports System.ComponentModel.Composition
    3 Imports System.ComponentModel.Composition.Hosting
    4 
    5 Public Class Program
    6    Private container As CompositionContainer
    7 
    8    <Import()> _
    9    Public Property MessageSender As IMessageSender
   10 
   11    <Import("Japanese")> _
   12    Public Property JapaneseMessageSender As IMessageSender
   13 
   14    <Import("English")> _
   15    Public Property EnglishMessageSender As IMessageSender
   16 
   17    Private Sub New()
   18       Me.container = New CompositionContainer()
   19       Dim batch As New CompositionBatch()
   20       batch.AddPart(New MessageSender())
   21       batch.AddPart(New JapaneseMessageSender())
   22       batch.AddPart(New EnglishMessageSender())
   23       batch.AddPart(Me)
   24       Me.container.Compose(batch)
   25    End Sub
   26 
   27    Private Sub Run()
   28       'IMessageSender型の取得  
   29       Dim messageSender As IMessageSender = Me.container.GetExportedValue(Of IMessageSender)()
   30       Console.WriteLine(messageSender.GetMessage())
   31 
   32       'JapaneseMessageSenderの取得  
   33       Dim japaneseMessageSender As IMessageSender = Me.container.GetExportedValue(Of IMessageSender)("Japanese")
   34       Console.WriteLine(japaneseMessageSender.GetMessage())
   35 
   36       'EnglishMessageSenderの取得  
   37       Dim englishMessageSender As IMessageSender = Me.container.GetExportedValue(Of IMessageSender)("English")
   38       Console.WriteLine(englishMessageSender.GetMessage())
   39 
   40       'インポートされたIMessageSender
   41       Console.WriteLine(Me.MessageSender.GetMessage())
   42 
   43       'インポートされたJapaneseMessageSender
   44       Console.WriteLine(Me.JapaneseMessageSender.GetMessage())
   45 
   46       'インポートされたEnglishMessageSender
   47       Console.WriteLine(Me.EnglishMessageSender.GetMessage())
   48    End Sub
   49 
   50    Public Shared Sub Main()
   51       Dim program As New Program()
   52       program.Run()
   53    End Sub
   54 End Class
   55 
   56 Public Interface IMessageSender
   57    Function GetMessage() As String
   58 End Interface
   59 
   60 <Export(GetType(IMessageSender))> _
   61 Public Class MessageSender
   62    Implements IMessageSender
   63    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   64       Return "MessageSenderからのメッセージです。"
   65    End Function
   66 End Class
   67 
   68 <Export("Japanese", GetType(IMessageSender))> _
   69 Public Class JapaneseMessageSender
   70    Implements IMessageSender
   71    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   72       Return "メッセージです。"
   73    End Function
   74 End Class
   75 
   76 <Export("English", GetType(IMessageSender))> _
   77 Public Class EnglishMessageSender
   78    Implements IMessageSender
   79    Public Function GetMessage() As String Implements IMessageSender.GetMessage
   80       Return "It is a message."
   81    End Function
   82 End Class

上記サンプルを実行すると以下の結果が出力されます。

 

contract_details_output

 

サンプルコードを通じてコントラクトの理解が深まりましたでしょうか?コントラクトでサービスの識別子を指定し使用する側で柔軟なサービスの利用が行えるようになります。コントラクトはMEFにおいて重要な物ですので是非ものにしてしまいましょう。

 

Source code