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