Managed Extensibility Framework(MEF)入門 -Custom Export-
MEFではエクスポート時にSystem.ComponentModel.Composition.ExportMetadataAttributeクラスを使用してメタデータの設定出来ます。メタデータ設定ではExportAttributeとExportMetadataAttributeを併用して実装する必要がありました。今回取り上げるカスタムエクスポートを作成するとエクスポート定義にメタデータ定義を実装する事が出来ます。
カスタムエクスポートクラスを作成するには、『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 }
VB1 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
上記サンプルコードの実行結果は以下の通りです。
上記ではコントラクトが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と変更した場合、出力結果は以下の通りになります。
メタデータビューのメタデータ名を変更した事によって、カスタムエクスポート属性クラスのメタデータと関連性が損なわれてしまった為に正しくメタデータを認識出来なくなってしまいました。又、この関連性の欠損はコンパイル時でも発見する事は出来ず、原因特定するのが困難な状況を生み出してしまいます。この問題は、メタデータビューとカスタムエクスポート属性クラスのメタデータをインターフェース制約を使用して解決する事が出来ます。そのサンプルコードは以下になります。
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クラスを使用せずに設定できます。またエクスポート定義もコンストラクタ引数を使用するように変更します。
このようにしてメタデータとカスタムエクスポート属性クラスのメタデータの関連付けをする事で見つけずらいバグを防ぐ事が出来ます。カスタムエクスポート属性クラスを作成する事で、明確でかつ修正変更に対応出来るエクスポートが実装出来る事がご理解頂けたかと思います。是非カスタムエクスポート属性クラスを活用してみては如何でしょうか。