かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Managed Extensibility Framework入門 その2「使うに当たって覚えておきたいこと」

さて、前回はMEFというよりはDIコンテナとはなんぞや?ということについて語ってしまうような内容になってしまいました。今回はMEFを使う上で知っておきたいクラスと、その役割について説明します。因みに、ここで使用するのは基本的にMEFを使うのに必要なものだけで、応用的なものは説明しません。さらに、動作についても簡単に説明しますがイメージをつかんでもらうための説明のため、実際に担ってる動きとは違う内容を話てるかもしれないので気を付けてください。

今回取り扱うクラス

  • DIコンテナ
    • System.ComponentModel.Composition.Hosting.CompositionContainer
  • カタログ役
    • System.ComponentModel.Composition.Hosting.AssemblyCatalog
    • System.ComponentModel.Composition.Hosting.TypeCatalog
    • System.ComponentModel.Composition.Hosting.DirectoryCatalog
    • System.ComponentModel.Composition.Hosting.AggregateCatalog
  • 型のエクスポートとインポート
    • System.ComponentModel.Composition.ExportAttribute
    • System.ComponentModel.Composition.ImportAttribute

DIコンテナ役のクラス

まず、MEFのDIコンテナ的な役割をする人を紹介します。

  • System.ComponentModel.Composition.Hosting.CompositionContainerクラス
    • このクラスがDIコンテナとして機能します。このコンテナに対してクラスのインスタンスを要求すると組み立てが完了したクラスが返ってきます。

カタログ

さて、DIコンテナのクラスの説明がすごくあっさりしてますが気にせず次へ行きます。DIコンテナは、クラスのインスタンスを作ったりクラスに対して関連のあるクラスのインスタンスをセットしたりということをします。この時、どんなクラスをコンテナで管理するかという型のカタログ情報を提供するのがカタログです。よく使うカタログのクラスは以下のようなものになります。

  • System.ComponentModel.Composition.Hosting.AssemblyCatalog
    • アセンブリ内にある型を取扱います。
  • System.ComponentModel.Composition.Hosting.DirectoryCatalog
    • ディレクトリ内にあるアセンブリ内にある型を扱います。
  • System.ComponentModel.Composition.Hosting.AggregateCatalog
    • 複数のカタログを束ねるのに使用します。

ExportとImport

さて、最後にExportとImportです。AssemblyCatalogはアセンブリ内にある型を取り扱うと説明しましたが、こいつは無差別にすべての型を取り扱うわけではありません。どの型を使うのかという情報を属性を使って指定します。そのために使う属性がExportとImportになります。Export属性を付けたクラスがAssemblyCatalogやDirectoryCatalogが扱う型になります。Exportだけでは、その型のクラスのインスタンスにどんなものを設定していいのかがわかりません。なのでImport属性を付けて、ここに値を設定してほしいという事をDIコンテナに伝えます。

結局どう動くのか

CompositionContainerのインスタンスを作成するときに、取り扱う型の情報を持ったカタログクラスを渡すことでコンテナが取り扱う型の種類を決定します。そして、コンテナからインスタンスを取得すると、ExportとImportで設定したルールに従って組み立て完了したクラスのインスタンスが取得できるようになります。

Hello MEF

ということで、文章だけではイメージしづらいと思うので、実際に上記のクラスを使って前回例にあげたAクラスとBクラスを使った簡単なハローワールド的なプログラムを書いてみます。適当なコンソールアプリケーションを作成します。ここでは、HelloMEFという名前でアプリケーションを作成してみました。

そして、以下の参照を追加します。

  • System.ComponentModel.Composition

次にクラスの定義を行います。小さなプログラムなので全部Program.csに書いてしまいます。まず、AクラスとBクラスを定義してExport属性をつけてAssemblyCatalogに登録する対象のクラスであることを明示します。

// Export属性を使ってカタログに収集してもらう対象にする
[Export]
public class A
{
}

[Export]
public class B
{
}

まだAクラスとBクラスの関連を定義していませんが、この状態でさっくりとコンテナの初期化を行ってインスタンスの取得までやってみようと思います。Mainメソッドの中に以下のコードを記述します。

// 現在実行中のアセンブリからカタログを作成する
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 現在実行中のアセンブリのカタログを元にコンテナを作る
var container = new CompositionContainer(catalog);

// Aクラスのインスタンスを取得する
var typeA = container.GetExportedValue<A>();

// 値が取れてるか確認
Console.WriteLine(typeA);

まず、AssemblyCatalogを現在実行中のアセンブリを使って生成します。現在のアセンブリ内には先ほど定義したExport属性をつけたAクラスとBクラスがいるので、このクラスがカタログ内にある状態になります。次に、CompositionContainerクラスをカタログクラスを指定して作成しています。そして、GetExportedValue<型>()というメソッドを使ってコンテナからインスタンスを取得します。最後に、値が取れているのか確認するためにコンソールに出力しています。


ここまでで、Program.csの中身は以下のようになりました。

namespace HelloMEF
{
    using System;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.Reflection;

    class Program
    {
        static void Main(string[] args)
        {
            // 現在実行中のアセンブリからカタログを作成する
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

            // 現在実行中のアセンブリのカタログを元にコンテナを作る
            var container = new CompositionContainer(catalog);

            // Aクラスのインスタンスを取得する
            var typeA = container.GetExportedValue<A>();

            // 値が取れてるか確認
            Console.WriteLine(typeA);
        }
    }

    // Export属性を使ってカタログに収集してもらう対象にする
    [Export]
    public class A
    {
    }

    [Export]
    public class B
    {
    }
}

実行すると、以下のような結果になります。

HelloMEF.A

ちゃんとAクラスのインスタンスが取得できていることがわかります。試しにAクラスにつけているExport属性を外してプログラムを実行してみると以下のような結果になります。

ハンドルされていない例外: System.ComponentModel.Composition.ImportCardinalityMis
matchException: 制約 '((exportDefinition.ContractName == "HelloMEF.A") AndAlso (
exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "HelloMEF.A"
.Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))' に一致する
有効なエクスポートが見つかりませんでした。無効なエクスポートが拒否されている可能
性があります。

以下StackTrace

色々小難しいことが書いてありますが、Exportが無い型を取得しようとすると怒られるということです。


次に、AクラスがBクラスを使うようにします。何通りか関連の定義の仕方はあるのですが、ここでは一番簡単なプロパティを使う方法を使って定義します。

// Export属性を使ってカタログに収集してもらう対象にする
[Export]
public class A
{
    // このプロパティの型に一致するクラスのインスタンスを
    // コンテナ内から探してセットしてという意味
    [Import]
    public B Value { get; set; }
}

[Export]
public class B
{
}

Aクラスにプロパティが追加されて、ここにImport属性がつけられています。このようにすることで、コンテナがAクラスがBクラスを必要としているということを認識するようになります。次に、AクラスとBクラスに適当なメソッドを定義して以下のようにしてみました。

// Export属性を使ってカタログに収集してもらう対象にする
[Export]
public class A
{
    // このプロパティの型に一致するクラスのインスタンスを
    // コンテナ内から探してセットしてという意味
    [Import]
    public B Value { get; set; }

    // Bクラスを使って処理をする
    public void Foo()
    {
        this.Value.Bar();
    }
}

[Export]
public class B
{
    public void Bar()
    {
        // 念願のメッセージを表示します
        Console.WriteLine("Hello MEF!!");
    }
}

AクラスのFooを呼ぶとBクラスのBarが呼ばれて念願のメッセージをコンソールに出力するという流れです。ではMainを以下のように書き換えて実行してみましょう。

// 現在実行中のアセンブリからカタログを作成する
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 現在実行中のアセンブリのカタログを元にコンテナを作る
var container = new CompositionContainer(catalog);

// Aクラスのインスタンスを取得する
var typeA = container.GetExportedValue<A>();

// メソッドを呼ぶ!
typeA.Foo();

実行結果は、おそらく皆さんが想像した通りになります。

Hello MEF!!

ここで、ちょっとBクラスについているExport属性を外して実行してみます。結果は以下のようになりました。

ハンドルされていない例外: System.ComponentModel.Composition.ImportCardinalityMis
matchException: 制約 '((exportDefinition.ContractName == "HelloMEF.A") AndAlso (
exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "HelloMEF.A"
.Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity"))))' に一致する
有効なエクスポートが見つかりませんでした。無効なエクスポートが拒否されている可能
性があります。

また難しいメッセージが書いてありますが、BクラスのExport属性をとっておきたエラーなのでAクラスにBクラスをセットしようとしたけどBクラスがカタログ内に無いため怒られています。そのため、コンテナから取得するクラス(今回の例の場合Aクラス)に設定するクラス(今回の例の場合Bクラス)もカタログに登録していないといけないということがわかります。コンテナが型を組み立てるのは、あくまでカタログ内に登録されている型でおこなっているということが理解できればOKです。

今回のまとめ

以下の言葉のイメージと、対応するクラスの使い方を覚えておく。

  • コンテナ
    • CompositionContainerクラス
  • カタログ
    • AssemblyCatalogクラス
    • DirectoryCatalogクラス(今回は未使用)
    • AggregateCatalogクラス(今回は未使用)
  • ExportとImport
    • Export属性
    • Import属性

MEFを使うときのプログラムの流れを覚えておく。

  • 使う型を定義する
  • カタログに登録する型にExport属性をつける
  • コンテナにセットして欲しいプロパティにImport属性をつける
  • カタログを作成する
    • var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
  • コンテナを作成する
    • var container = new CompositionContainer(catalog);
  • コンテナから値を取得する
    • var typeA = container.GetExportedValue<A>();


眠たくなってきたので今日はここらへんまでです。以上。