MSDN マガジン > Home > 発行物 > 2008 > July >  CLR 徹底解剖 : System.AddIn を使用して Windows フォーム アプリケ...
CLR 徹底解剖
System.AddIn を使用して Windows フォーム アプリケーションを拡張する
Mueez Siddiqui

コードのダウンロード : CLRInsideOut2008_07.exe (472 KB)
オンラインでのコードの参照
新しい Microsoft® .NET アドイン フレームワーク (System.AddIn) を使用して、Windows® フォーム アプリケーションに拡張性を付与できることをご存じでしょうか。今回のコラムでは、このアドイン フレームワークを通してオブジェクト モデルを公開できるように、ShapeApp という描画アプリケーションに変更を加えます。これにより、ホスト アプリケーション上でタスクを自動的に実行するアドイン (自動化アドイン) を作成できます。また、このシナリオで一般的に直面する問題の種類について説明し、その解決策を提示します。
記事を読み始める前にアドイン フレームワークに関する背景知識を必要とされる方は、最初に CLR アドイン チームのブログのリソース ページ (go.microsoft.com/fwlink/?LinkId=117519) をお読みになることをお勧めします。また、「CLR 徹底解剖」コラムで以前に取り上げられたアドイン フレームワークに関する記事も参考になります。
ShapeApp は、基本的な図形を使用して描画を行うことができる描画アプリケーションです。新しい図形を挿入して、それらを移動したり、色やサイズを変更したりできます。描画をファイルに保存することや、複数の描画をタブで開くこともできます。ShapeApp で作成した簡単な描画を図 1 に示します。
図 1 ShapeApp のスクリーンショット
ShapeApp は、Visual Studio® Tools for Applications (VSTA) チームにより、Visual Studio IDE をアプリケーションに埋め込むことができるサンプルとして開発されました。埋め込んだ IDE は、アプリケーションに対する拡張の記述に使用できます。このアプリケーションは、拡張性コード付きのバージョンと拡張性コードなしのバージョンが提供されています。このサンプルでは、基本バージョンから始めて、アドイン フレームワークと共に動作するよう調整を加えます。

ホストを調整する
このセクションでは、HTML のドキュメント オブジェクト モデル (DOM) に似たオブジェクト モデルを公開できるように、アプリケーションを調整します。それによって、アプリケーションのタスクを自動化するアドインを作成できます。
ホストを調整する最初の手順は、ホストが公開するオブジェクト モデルを定義することです (まだコードを記述する必要はないことに注意してください)。アドイン フレームワークの分離機能およびバージョン管理機能により、オブジェクト モデルに制限が加えられます。一般に、オブジェクト モデルは、アドインが使用する型の中で .NET Framework に組み込まれていないものをすべて定義する必要があります。つまり、ビュー アセンブリは、.NET Framework アセンブリ以外のアセンブリを参照しないようにする必要があります。MarshalByRefObject 型は、明示的に禁止されます。使用可能な型の詳細については、コントラクトに関するブログ記事 (go.microsoft.com/fwlink/?LinkId=117520) を参照してください。
ShapeApp のオブジェクト モデルの型の一覧を図 2 に示します。ここに示されるすべての型は、ホスト アプリケーションの内部に存在します。後で、これらの型をパイプライン経由で公開します。
説明
ShapeApplication メインのアプリケーション オブジェクト
Drawing アプリケーション内の描画を表すオブジェクト
Shape 描画内の図形を表すオブジェクト
DrawingCollection 描画オブジェクトのコレクション
ShapeCollection 図形オブジェクトのコレクション
EventArgs 関連の型 カスタム イベント引数に使用される、System.EventArgs から派生したいくつかの型
次の手順として、ホストがアドインを検出してアクティブ化するための手段を追加します。そのために、ホスト上で単純なアドイン マネージャを使用します。これには、アドインを読み込んで追跡する静的クラス AddInManager と、ユーザーがアドインを管理するためのフォームが含まれます。このフォームは、ホスト上のメニュー項目を通してアクティブ化され、ホストに対して必要な変更は最小限で済みます。このフォームを図 3 に示します。
図 3 アドイン マネージャ フォーム
アドイン マネージャのコードは非常に単純です。使用可能なアドインを見つけるために必要なコードは、次の 3 行です。
// update the add-in store to include add-ins located in the 
// current program folder
string[] warnings = AddInStore.Update(PipelineStoreLocation.ApplicationBase);
// get the view type defined in the host views assembly
System.Type hostViewOfAddIn = typeof(ShapeAppHostViews.IShapeAddIn);
// find all add-ins that implement the view
ICollection<AddInToken> addIns = AddInStore.FindAddIns(hostViewOfAddIn,
    PipelineStoreLocation.ApplicationBase);
最初の行 AddInStore.Update は、使用可能なアドインおよびパイプライン コンポーネントを検索するために使用します。PipelineStoreLocation.ApplicationBase は、アプリケーションのフォルダを検索するように指示しています。フォルダ構造の詳細については、go.microsoft.com/fwlink/?LinkId=117521 を参照してください。次に、アクティブ化するアドインの型を取得します。ここでは、ホスト ビュー アセンブリに定義された IShapeAddIn 型です。最後に、検査してアクティブ化できる使用可能なアドイン トークンのコレクションを返す AddInStore.FindAddIns を呼び出します。コレクションからトークンを選択したら、1 行のコードを使用してそれをアクティブ化できます。
// activate the add-in in full trust mode
ShapeAppHostViews.IShapeAddIn addIn = 
    token.Activate<ShapeAppHostViews.IShapeAddIn>( 
    AddInSecurityLevel.FullTrust);
また、アドインをプロセス外または異なる信頼レベルでアクティブ化することも選択できます。分離レベルの一覧については、go.microsoft.com/fwlink/?LinkId=117522 で確認してください。各分離レベルのパフォーマンスの比較については、パフォーマンスに関するブログ記事 (go.microsoft.com/fwlink/?LinkId=117523) を参照してください。

オブジェクト モデルを公開する
空のパイプラインとアドイン マネージャが準備できたら、ホストからアドインへの機能の公開を開始できます。オブジェクト モデルに含まれる型を公開するには、まず、公開する型のそれぞれに対して、HostViews アセンブリ内にホスト ビューを作成する必要があります。このビューでは、プロパティ、メソッド、およびイベントを公開できます。ホスト ビューには、抽象基本クラスまたはインターフェイスを使用できます。
ShapeApp では、3 つの理由からインターフェイスを使用しました。1 つは、Visual Basic® から使用されたときにイベントが正しく動作するには、インターフェイスが必要であるためです。2 つ目の理由は、C# が多重継承をサポートしないため、1 つのクラスが 2 つの基本クラスから継承できないことです。したがって、すべてのホスト クラスで実装できるようにするには (それらが既に基本クラスを持つ場合であっても)、ビューがインターフェイスである必要があります。3 つ目の理由は、インターフェイスでは EIMI (明示的なインターフェイス メソッドの実装) を使用できることです。この重要性については、後で説明します。
ShapeApplication オブジェクトのホスト ビューがどのようなものかを図 4 に示します。メンバで使用される型は、EventHandler<T> のような組み込みフレームワーク型か、または IDrawing のような他のビューです。CodePlex のサイト (go.microsoft.com/fwlink/?LinkId=117524) から FxCop ルールをダウンロードして、これらのビューを完全なパイプラインに簡単に組み込めることを確認できます。
namespace ShapeAppHostViews
{
    public interface IShapeApplication
    {
        // the drawing present in the selected tab
        IDrawing ActiveDrawing { get; }
        // a collection of available shapes (square, circle, etc)
        IShapeCollection AvailableShapes { get; }
        // a collection of drawings currently open in the application
        IDrawingCollection Drawings { get; }
        // main window visibility 
        bool Visible { get; set; }

        // create a new drawing in the application
        IDrawing NewDrawing();
        // completely exit the application
        void Quit();

        // event fired when a drawing is created
        event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
    }
}
ビュー アセンブリを作成したところで、対応するビューから継承するようにホストのクラスを修正します。ShapeApplication クラスへの変更箇所を次に太字で示します。
public class ShapeApplication : System.Windows.Forms.IWin32Window
{
...
}

public class ShapeApplication : ShapeAppHostViews.IShapeApplication,
    System.Windows.Forms.IWin32Window
{
...
}
次に、ホスト ビューから各メンバを実装する必要があります。引数および戻り値として組み込み型を使用するメンバに対しては、(そのメンバがホストのクラスでパブリックであることを確認する以外) 特別な実装は必要ありません。たとえば、ShapeApplication クラスの Visible プロパティは、Visible インターフェイス メンバと同じシグニチャを持ちます。
public bool Visible
{
    get {...}
    set {...}
}
これは、ホストの ShapeApplication クラスでの Visible の既存の実装が、ビューの実装として使用されることを意味します。
ただし、ホスト ビューに定義された他の型を使用するメンバは、明示的に実装される必要があります。たとえば、ActiveDrawing プロパティは、ShapeApplication クラス内と IShapeApplication ホスト ビュー内で異なるシグニチャを持ちます。ホストのクラスに含まれる元のバージョンのプロパティは、Drawing オブジェクトを返します。
public Drawing ActiveDrawing
{
    get {...}
}
ホスト ビュー内のバージョンである IShapeApplication は、対応するビュー IDrawing だけを返せば済みます。
IDrawing ActiveDrawing
{
    get;
}
この 2 番目のバージョンをホストのクラスに実装するために、EIMI を使用する必要があります。これは、ActiveDrawing という同じ名前の新しいプロパティをホストのクラスに追加することで行います。これで、ホストの ShapeApplication クラスには、ActiveDrawing というプロパティが 2 つ含まれることになります (図 5 を参照)。
// original property
public Drawing ActiveDrawing
{
    get {...}
}

// implementation of the host view's version of ActiveDrawing
ShapeAppHostViews.IDrawing 
ShapeAppHostViews.IShapeApplication.ActiveDrawing
{
    get 
    { 
       // a call to the original property
       return this.ActiveDrawing; 
    } 
}
新しい実装は、Drawing オブジェクトを IDrawing 型へと暗黙的にキャストします。Windows フォーム アプリケーションに対してオブジェクト モデルを公開する場合、公開されたメンバの一部が Windows フォーム コントロールにアクセスして変更を加える可能性が高くなります。そのようなメンバが、コントロールを作成したスレッド以外のスレッドから呼び出された場合、GUI の動作は予測不能になります。
アドインに公開されるメンバはどのスレッドからでも呼び出せるため、公開されたメンバが GUI コントロールにアクセスしたり変更を加えたりする際には、Control.InvokeRequired および Control.Invoke を使用して、処理が安全に行われるようにする必要があります。たとえば、ShapeApplication.NewDrawing 機能は ApplicationForm オブジェクトにアクセスするので、安全な実装のために Invoke を使用する必要があります。
次に古い実装を示します。
public Drawing NewDrawing()
{
    Drawing newDrawing = new Drawing(this); 
    ...
    this.ApplicationForm.drawingsTabControl.TabPages.Add(
        newDrawing.DrawingSurface);
    ... 
    return newDrawing;
}
新しい実装を図 6 に示します。変更箇所は太字で示しています。
                                                                                // delegate added to allow invoke
private delegate Drawing NewDrawingDelegate();
 
public Drawing NewDrawing()
{
    // check if we need an invoke
    if (this.ApplicationForm.InvokeRequired)
    {
        // invoke this method using the ApplicationForm object
        NewDrawingDelegate del = new NewDrawingDelegate(NewDrawing);
        return (Drawing) ApplicationForm.Invoke(del); 
    }
    else
    {
        Drawing newDrawing = new Drawing(this); 
        ...
        this.ApplicationForm.drawingsTabControl.TabPages.Add(
            newDrawing.DrawingSurface);
        ... 
        return newDrawing;
    }
}

アダプタ、コントラクト、およびアドイン ビュー
ホストによって公開された機能にアドインからアクセスできるようにするため、アダプタ、コントラクト、およびアドイン ビューを作成する必要があります。パイプライン コンポーネントは、ホスト ビューから自動的に生成できます。Pipeline Builder は、この自動生成だけを実行するためのツールです。これは go.microsoft.com/fwlink/?LinkId=117525 から入手できます。Pipeline Builder は、このアプリケーションの最初のバージョンに対して適切に動作します。
または、パイプラインを手動でコーディングすることもできます。これは、複数バージョンに対応したアダプタを記述するときに必要です。Visual Studio では、必要な各アセンブリ (ビュー、アダプタ、コントラクトなど) に対するプロジェクトを ShapeApp ソリューションに追加することで、パイプラインを設定できます。Visual Studio でパイプラインを作成する手順のステップバイステップ ガイドについては、go.microsoft.com/fwlink/?LinkId=117526 を参照してください。
このサンプルでは、パイプラインを手動でコーディングしています。これは ShapeApp オブジェクト モデルの最初のバージョンであるため、ホストとアドインのビューは同じです。したがって、両方のビューに対して 1 つのアセンブリを使用できます。手動でコーディングする場合には、最初に空のパイプラインを設定すると便利です。そのようなパイプラインのビューは次のようになります。
public interface IShapeAddIn
{
    void Initialize(IShapeApplication application);
}

public interface IShapeApplication
{
    // TODO: Implement this.
}
Initialize メソッドは、読み込み時にメイン アプリケーション オブジェクトをアドインに渡すために使用されます。これにより、ホストが明示的に何らかのサービスを要求しなくても、アドインでいつでもホストを制御できるようになります (これが、自動化アドインと呼ばれる理由です)。
空のパイプラインを作成した後は、ホストから他の機能を 1 つずつ公開し、それをコンパイルしてテストできます。この反復的なアプローチにより、コンパイル エラーが果てしなく検出されるような事態を避けることができます。もちろん、いったんインターフェイスが完成し、アプリケーションがリリースされた後は、既存のインターフェイスにそれ以上メソッドを追加することはできません。どのようなアダプタが必要となるかは、オブジェクトがどこに存在し、どこでアクセスされるかによって決まります。一般に、オブジェクトは次の 3 つのカテゴリのいずれかに含まれます。
ホスト側オブジェクト これらのオブジェクトは、ホスト側に存在し、アドインからアクセスされます (ShapeApplication クラスなど)。このようなオブジェクトをアドインに対して公開するには、図 7 に示すようなパイプライン コンポーネントが必要です。オブジェクトはホスト側に存在するため、上部のホスト ビューから開始します。ビューからコントラクトへのホスト アダプタを使用してビューをコントラクトに変換し、コントラクトからビューへのアドイン アダプタを使用してアドイン ビューに変換します。この 2 つのアダプタにより、アドインはホスト側オブジェクトにアクセスできます。
ホスト側オブジェクト アドイン側オブジェクト
ホスト (オブジェクトはここに存在) アドイン (オブジェクトはここに存在)
ホスト ビュー アドイン ビュー
ビューからコントラクトへのホスト アダプタ ビューからコントラクトへのアドイン アダプタ
コントラクト コントラクト
コントラクトからビューへのアドイン アダプタ コントラクトからビューへのホスト アダプタ
アドイン ビュー ホスト ビュー
アドイン (ここからオブジェクトにアクセス) ホスト (ここからオブジェクトにアクセス)
アドイン側オブジェクト これらのオブジェクトは、アドイン側に存在し、ホストからアクセスされます (ShapeAddIn クラスなど)。ここではパイプライン コンポーネントは互いに似ていますが、ホストからアドイン側オブジェクトにアクセスできるように、アダプタの方向が逆になっています。ビューからコントラクトへのアダプタはホスト側ではなくアドイン側にあり、コントラクトからビューへのアダプタはホスト側にあります。パイプラインの残りの部分 (ビューおよびコントラクト) は同じであることに注意してください。
両側オブジェクト これらのオブジェクトは、どちらの側にも存在でき、どちらの側からでもアクセスできます。両方向に適用する必要があるため、このようなオブジェクトには各側に 2 つ、合計 4 つのアダプタが必要となります。ホストとアドインのビューに同じアセンブリを使用している場合、アダプタも再利用できることに注意してください。その場合、必要なアダプタは 2 つで済みます。
コントラクトは、イベントをネイティブではサポートしていません。ただし、次に示すパターンを使用して、コントラクト内でイベントをシミュレートできます (IShapeApplication.CreatedDrawing イベントの例)。
ホスト ビュー :
public interface IShapeApplication
{
       ... 
       // event fired when a drawing is created 
       event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
対応するコントラクト :
public interface IShapeApplicationContract : IContract
{
    ...
    void CreatedDrawingAdd(ICreatedDrawingEventHandlerContract handler);
    void CreatedDrawingRemove(ICreatedDrawingEventHandlerContract 
        handler);
}

public interface ICreatedDrawingEventHandlerContract : IContract
{
    void Handler(ICreatedDrawingEventArgsContract args);
}
ご覧のとおり、コントラクトでは System.EventHandler<CreatedDrawingEventArgs> のようなデリゲートを使用できないため、イベント ハンドラに対して新しいコントラクト ICreatedDrawingEventHandlerContract を作成しています。
アドイン側アダプタは、Add および Remove メソッドを使用してホスト側にハンドラを登録し、アドインがサブスクライブできるローカル イベントを保持します。ホスト側アダプタは、イベントが発生すると Handler 関数を呼び出します。
このような複雑な追加部分は、アドイン開発者に対しては大部分が透過的ですが、1 つ注意が必要です。アドインが自分自身を登録するときと登録解除するときには、オブジェクトの同じアダプタ インスタンスで行う必要があります。そうしないと、登録解除が有効になりません。
次に、ホスト オブジェクトがどのようにして、それを参照するアダプタ オブジェクトを 2 セット (またはそれ以上) 持つことができるのかを説明します。ホスト側のオブジェクトが (プロパティのアクセスまたは関数呼び出しを通して) アドインに返されると、ホスト オブジェクトへのアクセスを可能にする 2 つのアダプタ オブジェクト (ホスト側アダプタとアドイン側アダプタ) が作成されます。同じオブジェクトが再度アドインに返されると、2 つの新しいアダプタが作成されます。
たとえば、ShapeApplication.ActiveDrawing に 2 回アクセスすると、2 つの異なるオブジェクト参照がアドインに返され、ShapeApplication.ActiveDrawing.ReferenceEquals(ShapeApplication.ActiveDrawing) は false を返します。同じホスト オブジェクトに対して 2 つ (またはそれ以上) のアダプタが存在することは、イベントを登録/登録解除したり、ホスト オブジェクトをコレクションに格納したりするときなどに、問題となる可能性があります。
これらの問題を解決するために、アダプタ上で .Equals および .GetHashCode 関数をオーバーライドして、実際のホスト オブジェクト上の対応する関数を呼び出すようにできます。これにより、ホスト オブジェクトをアドイン内のコレクションに配置することができ、.Contains などのメソッドが適切に機能します。もちろん、その場合でも、アドイン開発者はイベントの登録と登録解除が同じオブジェクト上で行われるよう注意する必要があります。また、.Equals 関数がそのために役立つことも知っておいてください。
既にお気付きかもしれませんが、もう 1 つのオプションとして、アダプタをキャッシュすることも考えられます。ただし、オブジェクトからアダプタへの弱いマッピングを格納する簡単な方法がないため、これは実際には困難です (ここで、"弱い" とは、ガベージ コレクタによって無視される弱いオブジェクト参照のことを指します)。標準の辞書を使用することで、アダプタやオブジェクトに対するガベージ コレクションの実行を避けることができます。

アドインを記述する
パイプラインが準備できたら、アドイン ビューに対してアドインを記述できます。アドインの記述は、非常に簡単です。アドイン開発者が行うのは、アドイン ビューから継承するクラスを作成し、アドイン実装を AddIn 属性でマークすることだけです。残りのコードは、ホストとアドインの間にパイプラインが存在しない場合とほとんど同様に記述できます。ただし、注意すべき点がいくつかあります。1 つは、前に述べたオブジェクトの種類の問題であり、もう 1 つは、どのような分離境界が使用されるかに応じたパフォーマンスの問題です。パフォーマンスのベンチマークについては、go.microsoft.com/fwlink/?LinkId=117527 を参照してください。
現在のところ、アドインがホスト アプリケーションのフォーム上に Windows フォーム コントロールを直接表示することはできません。ただし、次の 3 つの方法のいずれかを使用できます。アドイン独自のフォームを表示するか (インターネット信頼レベルなど、アドインが何らかの部分的な信頼状況で動作している場合には、アドイン フォームの読み込み時にユーザーに対して警告が表示されます)、ホスト アプリケーションのフォーム上で直接 Windows Presentation Foundation (WPF) コントロールを表示するか (go.microsoft.com/fwlink/?LinkId=117528 を参照)、または WPF コンテナにラップされた Windows フォーム コントロールを使用するか (go.microsoft.com/fwlink/?LinkId=117529 を参照) のいずれかです。
Windows フォームでは、スレッドに関していくつかの要件を満たす必要があります。たとえば、コマンドライン アプリケーションを拡張するアドインは、フォームを構築してそのイベントを処理するために新しいスレッドを作成する必要があります。これは、コマンドライン アプリケーションが既定でマルチスレッド アパートメント (MTA) モデルを使用する一方、Windows フォームでは UI スレッドに対してシングルスレッド アパートメント (STA) モデルを必要とするためです。この解決方法は単純です。Windows フォーム アプリケーションを拡張するアドインに、フォームを表示するための次のような 2 行のコードを含めるだけです (アドインがホストの UI スレッドからアクティブ化されて使用されると仮定しています)。
AddInForm form = new AddInForm();
form.Show();

コラボレーション アドイン
公開されたインターフェイスを使用すると、非常に強力なアドインを作成できます。その一例が、このサンプルに含まれるコラボレーション アドインです。このアドインを使用すると、2 つの異なるマシンを使用している 2 人の ShapeApp ユーザーが、リアルタイムで同時に描画を編集できます。コラボレーション アドインは、Windows Communication Foundation (WCF) を通して互いに接続し、この対話がインターネット経由でグローバルに動作します。接続画面のスクリーンショットを図 8 に示します。
図 8 コラボレーション アドインの UI
2 つのアドインが互いに接続されると、どちらかの側で作成されるかまたは開かれた新しいドキュメントがすべて共有されます。つまり、一方のマシン上での変更がリアルタイムでもう一方のマシンに送信されます。これは、イベントを使って実現されます。コラボレーション アドインが読み込まれると、アドインはアプリケーションのすべてのイベントに対してサブスクライブします。新しい描画が作成されると、CreatedDrawing イベントが発生します。アドインはこのイベントを受信し、新しい描画に対するすべてのイベントにサブスクライブします。同様に、図形の作成時には、図形に関連するすべてのイベントにサブスクライブします。これにより、アドインはすべてのユーザー アクションを追跡し、それをもう一方のアドインに伝播できます。
コラボレーション アドインを通してイベントが渡される経路を図 9 に示します。マシン 1 で、ユーザーがアクションを実行します (図形の位置の変更など)。これにより、ホストはイベントを発生させ、コラボレーション アドインがそれを受信します。コラボレーション アドインはメッセージを作成し、WCF 経由でマシン 2 のアドインに送信します。このアドインは、ホスト上で同じアクションを実行します。ホスト上でアクションを実行するときに、アドインがイベント ハンドラを一時的にアンフックすることに注意してください。これにより、イベントがアドインに戻されて無限のサイクルに陥ることを防ぎます。
図 9 コラボレーション アドイン経由でイベントが渡される経路 (クリックすると拡大画像が表示されます)
WCF を使用して、さらに興味深いシナリオを実現できます。コラボレーション アドインはサービスをホストするので、任意の数のクライアントがアドインに接続できます。これにより、3 人以上のユーザーが 1 つの描画に対してシームレスに共同作業できます。3 台のマシン間の接続を図 10 に示します。各マシン上のコラボレーション アドインが、他のすべてのアドインと接続されています。
図 10 ShapeApp の 3 つのインスタンスの接続 (クリックすると拡大画像が表示されます)

以上、.NET アドイン フレームワークを使用してアドインをホストするために ShapeApp を調整する方法を見てきました。アドイン フレームワークの能力や、ShapeApp をリアルタイムの共同エディタへとシームレスに変換するアドインの作成方法について、ある程度の知識が得られたのではないでしょうか。フィードバックや質問などがありましたら、アドイン チームのブログ (go.microsoft.com/fwlink/?LinkId=117530) までお気軽にお寄せください。

Mueez Siddiqui は、マイクロソフトの CLR セキュリティおよび拡張性チームのソフトウェア開発エンジニアです。

Page view tracker