WPF

フォールト トレラントな複合アプリケーションを構築する

Ivan Krivyakov

コード サンプルをダウンロードする

複合アプリケーションには幅広い需要がありますが、フォールト トレランスの要件にはそれぞれ違いが見られます。プラグインの 1 つにエラーがあるとアプリケーション全体が停止しても問題がないシナリオもあれば、それを許容できないシナリオもあります。ここでは、フォールト トレラントな複合デスクトップ アプリケーションのアーキテクチャについて説明します。今回紹介するアーキテクチャでは、各プラグインをプラグイン固有の Windows プロセスで実行することで高度な分離を実現します。このアーキテクチャは次の設計目標を念頭に置いて構築しています。

  • ホストとプラグインを厳密に分離する
  • プラグイン コントロールとホスト ウィンドウの表示を完全に統合する
  • 新しいプラグインを簡単に開発できる
  • 既存のアプリケーションをプラグインに簡単に変換できる
  • ホストから提供されるサービスをプラグインで使用できる (その逆も可能)
  • 新しいサービスやインターフェイスを簡単に追加できる

付属のソース コード (msdn.microsoft.com/magazine/msdnmag0114、英語) には、WpfHost.sln と Plugins.sln という 2 つの Visual Studio 2012 ソリューションを含めています。まずホストをコンパイルしてから、次にプラグインをコンパイルします。メインの実行可能ファイルは WpfHost.exe です。プラグイン アセンブリはオンデマンドで読み込みます。図 1に完成したアプリケーションを示します。


図 1 ホスト ウィンドウとアウト プロセスのプラグインとのシームレスな統合

アーキテクチャの概要

ホストでは、左上隅の領域にタブ コントロールと [+] ボタンを表示し、使用可能な一連のプラグインを示します。プラグインのリストは plugins.xml という XML ファイルから読み取りますが、別にカタログを実装することもできます。各プラグインはプラグイン固有のプロセスで実行するため、ホストにはプラグイン アセンブリを読み込みません。このアーキテクチャの概要を図 2 に示します。


図 2 アプリケーション アーキテクチャの概要

内部的には、このプラグイン ホストは、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) のパラダイムに従う通常の Windows Presentation Foundation (WPF) アプリケーションです。モデル部分は PluginController クラスで表し、読み込むプラグインのコレクションを保持します。読み込むプラグインは Plugin クラスのインスタンスで表し、それぞれ 1 つのプラグイン コントロールを保持し、1 つのプラグイン プロセスとやり取りを行います。

ホスト システムは、図 3のように編成した 4 つのアセンブリで構成されています。


図 3 ホスト システムのアセンブリ

WpfHost.exe はホスト アプリケーション、PluginProcess.exe はプラグイン プロセスです。このプロセスのインスタンス 1 つにつき 1 つのプラグインを読み込みます。WpfHost.Interfaces.dll はホスト、プラグイン プロセス、およびプラグインで使用する共通のインターフェイスを保持し、PluginHosting.dll には、プラグインをホストするためにホストとプラグイン プロセスで使用する型を保持します。

プラグインを読み込む場合、UI スレッドで実行しなければならない呼び出しもあれば、どのスレッドでも実行できる呼び出しもあります。アプリケーションの応答性を高めるには、どうしても必要な場合のみ UI スレッドをブロックします。したがって、Plugin クラスのプログラミング インターフェイスを Load と CreateView という 2 つのメソッドに分割しています。

class Plugin
 {
   public FrameworkElement View { get; private set; }
   public void Load(PluginInfo info); // Can be executed on any thread
   public void CreateView();          // Must execute on UI thread
 }

Plugin.Load メソッドはプラグイン プロセスを開始し、プラグイン プロセス側でインフラストラクチャを作成します。このメソッドはワーカー スレッドで実行します。Plugin.CreateView メソッドは、ローカルでの表示をリモートの FrameworkElement と結び付けます。このメソッドは、InvalidOperationException などの例外を回避するために UI スレッドで実行する必要があります。

Plugin クラスは、最後にプラグイン プロセス内でユーザー定義のプラグイン クラスを呼び出します。このユーザー クラスに関する要件は、WpfHost.Interfaces アセンブリの IPlugin インターフェイスを実装することだけです。

public interface IPlugin : IServiceProvider, IDisposable
 {
   FrameworkElement CreateControl();
 }

プラグインから返されたフレームワーク要素は、任意の複雑さにすることが可能で、単一のテキスト ボックスにしたり、基幹業務 (LOB) アプリケーションを実装する複雑なユーザー コントロールにしたりすることができます。

複合アプリケーションのニーズ

ここ数年間、筆者のクライアントの多くが同じビジネス ニーズを話題にしています。そのビジネス ニーズとは、外部プラグインを読み込んで、複数の LOB アプリケーションを 1 つの "屋根" の下で組み合わせるデスクトップ アプリケーションです。このようなアプリケーションが必要となる根本的な理由はさまざまです。複数のチームがアプリケーションのさまざまなパーツを異なるスケジュールで作成することもあれば、複数のビジネス ユーザーがさまざまな機能セットを必要とすることもあります。または、クライアントが柔軟性を維持しながら "コア" アプリケーションの安定性を確保したいと考える場合もあります。いずれにせよ、さまざまな組織で、サードパーティ製のプラグインをホストする必要性が頻繁に生じています。

このような課題には、複合アプリケーション ブロック (CAB)、Managed Add-In Framework (MAF)、Managed Extensibility Framework (MEF)、および Prism といった伝統的なソリューションがあります。MSDN の 2013 年 8 月号では、筆者の元同僚である Gennady Slobodsky と Levi Haskell によって別のソリューションが公開されています (msdn.microsoft.com/magazine/dn342875の「サードパーティの .NET プラグインをホストするためのアーキテクチャ」を参照してください)。どれも高品質なこれらのソリューションは、いくつもの有益なアプリケーションを生み出すのに使用されています。筆者もそのフレームワークのアクティブ ユーザーですが、かなり長い間悩まされている問題が 1 つあります。それは安定性です。

アプリケーションがクラッシュすることは、紛れもない事実です。Null 参照、ハンドルされない例外、ロックされたファイル、そして破損したデータベースがただちになくなることはありません。優れたホスト アプリケーションにするには、プラグインのクラッシュを切り抜けて動作を続けられるようにしなければなりません。問題のあるプラグインによってホストや他のプラグインが停止しないようにする必要があります。悪意のあるハッキング行為を防止しようというわけではないので、この保護は万全でなくてもかまいません。ただし、ワーカー スレッドで発生するハンドルされない例外などの単純なミスでホストが停止しないようにすることは必要です。

分離のレベル

Microsoft .NET Framework アプリケーションでは、少なくとも 3 つの方法でサードパーティ製のプラグインを処理できます。

  • 分離なし: 1 つの AppDomain を使用して、1 つのプロセスでホストとすべてのプラグインを実行します。
  • 中程度の分離: プラグイン固有の AppDomain に各プラグインを読み込みます。
  • 厳密な分離: プラグイン固有のプロセスに各プラグインを読み込みます。

分離なしは、保護と制御のレベルが最も低くなります。すべてのデータがグローバルにアクセス可能となり、エラー保護は行われず、問題を引き起こしているコードをアンロードすることもできません。アプリケーションがクラッシュする最も典型的な原因は、プラグインが作成するワーカー スレッドでハンドルされない例外が発生することです。

try/catch ブロックを使用してホスト スレッドの保護を組み込むことはできますが、プラグインが作成されたスレッドでは保護は機能しません。.NET Framework 2.0 からは、どのスレッドでハンドルされない例外が発生しても、そのプロセスは無条件に終了するようになりました。このように冷酷にも思える処理には相応の理由があります。ハンドルされない例外の発生は、アプリケーションが不安定になっている可能性が高いために続行するのは危険であることを意味しています。

中程度の分離では、プラグインのセキュリティと構成をより細かく制御できます。また、(少なくとも処理が順調で、アンマネージ コードを実行しているスレッドがないときには) プラグインをアンロードすることもできます。ですが、ホスト プロセスは依然としてプラグインのクラッシュから保護されていません。これは、以前「AppDomain ではエラーのあるプラグインからホストを保護できない」(bit.ly/1fO7spO、英語) で示したとおりです。信頼できるエラー処理戦略の設計は不可能ではありませんが難易度が高いうえ、エラーのある AppDomain のアンロードは保証されません。

AppDomain は、プロセスに代わる軽量のツールとして ASP.NET アプリケーションをホストするために作成されました。AppDomain については、bit.ly/PoIX1rで Chris Brumme による 2003 年のブログ記事「AppDomains ("application domains")」(AppDomains ("アプリケーション ドメイン")、英語) を参照してください。ASP.NET では、フォールト トレランスに対して比較的傍観者的なアプローチがとられています。Web アプリケーションがクラッシュすると、ワーカー プロセス全体が停止し、それに伴って複数のアプリケーションが停止します。この場合、ASP.NET は単純にワーカー プロセスを再開して、保留中のすべての Web 要求を再発行します。ユーザーに表示する独自のウィンドウを使用しないサーバー プロセスでは設計上妥当な決めごとといえます (ただしデスクトップ アプリケーションでは同じように機能しない場合があります)。

厳密な分離では、エラーに対する最大級の保護が提供されます。各プラグインは、プラグイン固有のプロセスで実行されるためにホストのクラッシュを引き起こすことがなく、任意の時点で終了できます。ただし、このソリューションの設計はやや複雑になります。アプリケーションでは、数多くのプロセス間通信と同期を処理しなくてはなりません。また、WPF コントロールをプロセスの境界を超えてマーシャリングすることも必要です。これは簡単な作業ではありません。

ソフトウェア開発における他の作業と同様、分離レベルの選択はトレードオフです。分離が厳密であるほど高度な制御と柔軟性がもたらされますが、その代償としてアプリケーションの複雑さが増し、パフォーマンスが低下します。

フォールト トレランスを無視して、"分離なし" レベルで処理をするフレームワークもあります。このアプローチの良い例が MEF と Prism です。フォールト トレランスやプラグイン構成の微調整が問題でなければ、機能するソリューションではこれが最も単純であるため、使用するのにふさわしいソリューションです。

Slobodsky と Haskell が提案しているものも含め、多くのプラグイン アーキテクチャでは中程度の分離が使用され、AppDomain によって分離が実現されます。AppDomain を使用する場合、ホストの開発者はプラグインの構成とセキュリティをかなり細かく制御できます。個人的には、ここ数年で AppDomain ベースのソリューションをいくつか作成しました。アプリケーションでコードのアンロード、サンドボックス化、および構成の制御が必要な場合 (およびフォールト トレランスが問題でない場合) は、AppDomain を使用することを強くお勧めします。

ホストの開発者が 3 つの分離レベルを自由に選択できる MAF は、アドイン フレームワークの中でも際立った存在です。MAF では、AddInProcess クラスを使用して、アドインをアドイン固有のプロセスで実行できます。残念なことに、AddInProcess はそのままでは表示コンポーネントで機能しません。MAF を拡張して表示コンポーネントをプロセス間でマーシャリングできる可能性はありますが、既に複雑なフレームワークがさらに複雑化することになります。MAF アドインの作成は簡単ではないうえ、MAF を拡張すれば、その複雑さは手に負えないほどになるでしょう。

今回紹介するアーキテクチャでは、足りない部分を補完して固有のプロセスでプラグインを読み込む堅牢なホスト ソリューションを提供することを目的とし、プラグインとホストの表示を統合します。

表示コンポーネントの厳密な分離

プラグインの読み込みが要求されると、ホスト プロセスは新しい子プロセスを開始します。その後、この子プロセスは、ホストに表示される FrameworkElement を作成するユーザー プラグイン クラスを読み込みます (図 4参照)。


図 4 プラグイン プロセスとホスト プロセス間での FrameworkElement のマーシャリング

FrameworkElement をプロセス間で直接マーシャリングすることはできません。MarshalByRefObject から継承しておらず Serializable としてマークされてもいないため、.NET リモート処理ではマーシャリングできません。また、ServiceContract 属性が付いていないため Windows Communication Foundation (WCF) によるマーシャリングもできません。この問題に対処するには、MAF に同梱されている System.Windows.Presentation アセンブリの System.Addin.FrameworkElementAdapters クラスを使用します。このクラスは 2 つのメソッドを定義します。

  • ViewToContractAdapter メソッド: FrameworkElement を、.NET リモート処理でマーシャリングできる INativeHandleContract インターフェイスに変換します。このメソッドは、プラグイン プロセス内で呼び出します。
  • ContractToViewAdapter メソッド: INativeHandleContract インスタンスを FrameworkElement に再度変換します。このメソッドはホスト プロセス内で呼び出します。

残念ながら、この 2 つのメソッドを単純に組み合わせるだけではうまく機能しません。MAF は、プロセス間ではなく AppDomain 間で WPF コンポーネントをマーシャリングするように作られているようです。ContractToViewAdapter メソッドではクライアント側で次のエラーが発生し、実行できません。

System.Runtime.Remoting.RemotingException:
 Permission denied: cannot call non-public or static methods remotely

この根本原因は、ContractToViewAdapter メソッドが、クラス コンストラクターの MS.Internal.Controls.AddInHost を呼び出しているためです。MS.Internal.Controls.AddInHost は、INativeHandleContract リモート処理プロキシを AddInHwndSourceWrapper 型にキャストしようとします。キャストが成功すると、リモート処理プロキシで内部メソッド RegisterKeyboardInputSite を呼び出します。プロセス間プロキシの内部メソッドを呼び出すことはできません。AddInHost クラス コンストラクター内部の処理を以下に示します。

// From Reflector
 _addInHwndSourceWrapper = contract as AddInHwndSourceWrapper;
 if (_addInHwndSourceWrapper != null)
 {
   _addInHwndSourceWrapper.RegisterKeyboardInputSite(
     new AddInHostSite(this)); // Internal method call!
 }

このエラーを取り除くために、ここでは NativeContractInsulator クラスを作成しました。このクラスはサーバー (プラグイン) 側に配置します。このクラスでは、ViewToContractAdapter メソッドから返される元の INativeHandleContract にすべての呼び出しを渡すことで、INativeHandleContract インターフェイスを実装します。ただし、元の実装とは違い AddInHwndSourceWrapper にキャストすることはできません。したがって、クライアント (ホスト) 側でのキャストは失敗し、禁止されている内部メソッドの呼び出しは実行されません。

プラグイン アーキテクチャのさらに詳細な検証

Plugin.Load メソッドと Plugin.CreateView メソッドは、プラグインの統合に必要なすべての動的パーツを作成します。

図 5 は、結果のオブジェクト グラフです。やや複雑ですが、各パーツには特定の役割があります。これらのパーツが合わさることで、ホスト プラグイン システムでシームレスかつ堅牢な処理が実現します。


図 5 読み込まれるプラグインのオブジェクト ダイアグラム

Plugin クラスは、ホストに存在する 1 つのプラグイン インスタンスを表します。このクラスには、ホスト プロセス内にあるプラグインの表示を受け持つ View プロパティがあります。Plugin クラスは、PluginProcessProxy のインスタンスを作成してそのインスタンスを IRemotePlugin から取得します。IRemotePlugin には、リモートのプラグイン コントロールが INativeHandleContract として含まれています。したがって、Plugin クラスは、以下のようにこのコントラクトを受け取って FrameworkElement に変換します (簡潔にするため一部のコードは省略しています)。

public interface IRemotePlugin : IServiceProvider, IDisposable
 {
   INativeHandleContract Contract { get; }
 }
 class Plugin
 {
   public void CreateView()
   {
     View = FrameworkElementAdapters.ContractToViewAdapter(
       _remoteProcess.RemotePlugin.Contract);
   }}

PluginProcessProxy クラスは、ホスト内からプラグイン プロセスのライフサイクルを制御するクラスで、プラグイン プロセスの開始、リモート処理チャネルの作成、およびプラグイン プロセスの正常性の監視を行います。また、PluginLoader サービスを使用してこのサービスから IRemotePlugin を取得します。

PluginLoader クラスは、プラグイン プロセスで実行し、プラグイン プロセスのライフサイクルを実装します。リモート処理チャネルの確立、WPF メッセージ ディスパッチャーの起動、ユーザー プラグインの読み込み、RemotePlugin インスタンスの作成、および RemotePlugin インスタンスをホスト側の PluginProcessProxy に渡す処理を行います。

RemotePlugin クラスを使用すると、プロセスの境界を超えてユーザー プラグイン コントロールをマーシャリングできるようになります。このクラスは、前述した不適切なメソッド呼び出しの問題に対処するため、ユーザーの FrameworkElement を INativeHandleContract に変換した後、このコントラクトと NativeHandleContractInsulator をラップします。

最後に、ユーザーのプラグイン クラスが IPlugin インターフェイスを実装します。IPlugin インターフェイスの主な役割は、プラグイン プロセス内でプラグイン コントロールを作成することです。通常、このコントロールは WPF UserControl となりますが、任意の FrameworkElement にすることが可能です。

プラグインの読み込みが要求されると、PluginProcessProxy クラスは新しい子プロセスを開始します。子プロセスの実行可能ファイルは、プラグインが 32 ビットか 64 ビットかによって PluginProcess.exe または PluginProcess64.exe のどちらかになります。各プラグイン プロセスは、コマンド ラインで一意の GUID とプラグインのベース ディレクトリを受け取ります。

PluginProcess.exe
   PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18
   c:\plug-in\assembly.dll

プラグイン プロセスは、IPluginLoader 型のリモート処理サービスを設定し、"ready" というイベント (今回の例では PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18.Ready) を発生させます。これで、ホストが IPluginLoader メソッドを使用してプラグインを読み込めるようになります。

ホストの準備が整ったときにプラグイン プロセスからホストを呼び出すソリューションもあります。このソリューションでは ready イベントが不要になりますが、エラー処理が大幅に複雑化します。"プラグインの読み込み" 処理の開始元がプラグイン プロセスの場合は、エラー情報もプラグイン プロセスに保持されます。ホストでは、問題が発生しても認識できない場合があることから、今回は ready イベントを使用する設計にしました。

設計上のもう 1 つの問題は、WPF ホスト ディレクトリ下に配置されていないプラグインに適合するかどうかでした。.NET Framework ではアプリケーション ディレクトリにないアセンブリを読み込むことで特定の問題が発生する一方、プラグインには配置に関してそのプラグイン固有の考慮事項があり、WPF ホスト ディレクトリ下にプラグインを配置できるとは限らない場合があることがわかりました。また、複合アプリケーションの中には、ベース ディレクトリから実行しないと適切に動作しないものもあります。

これらの考慮事項から、WPF ホストでは、ローカル ファイル システムのどこからでもプラグインを読み込めるようになっています。これを実現するため、プラグイン プロセスは、アプリケーション ベース ディレクトリをプラグインのベース ディレクトリに設定したセカンダリ AppDomain でほぼすべての処理を実行します。このようにすると、WPF ホスト アセンブリをその AppDomain に読み込むという課題が生まれます。この課題は、少なくとも 4 つの方法で解決できます。

  • WPF ホスト アセンブリをグローバル アセンブリ キャッシュ (GAC) に格納する。
  • プラグイン プロセスの app.config ファイルでアセンブリのリダイレクトを使用する。
  • LoadFrom/CreateInstanceFrom オーバーライドのいずれかを使用して WPF ホスト アセンブリを読み込む。
  • アンマネージ ホスト API を使用して、目的の構成を使用したプラグイン プロセスで CLR を起動する。

上記のソリューションにはそれぞれ長所と短所があります。WPF ホスト アセンブリを GAC に格納するには管理者のアクセス許可が必要です。GAC が簡潔なソリューションであるとはいえ、インストールに管理者権限が必要となると、企業環境では大きな頭痛の種となる場合があるため、今回はこのソリューションは見送りました。アセンブリのリダイレクトも魅力的ですが、構成ファイルが WPF ホストの場所によって異なるため、xcopy によるインストールができなくなります。アンマネージ ホスト プロジェクトの作成はメンテナンスの際に大きなリスクになります。

以上のことから、今回は LoadFrom を使用するアプローチを選択しました。このアプローチの大きな欠点は、WPF ホスト アセンブリが LoadFrom コンテキストになることです (bit.ly/cZmVuzで Suzanne Cook のブログ記事「Choosing a Binding Context」(バインド コンテキストを選択する、英語) を参照してください)。プラグインの AppDomain では、バインドの問題を回避するため、プラグインのコードで WPF アセンブリを見つけやすくなるように AssemblyResolve イベントをオーバーライドする必要がありました。

プラグインの開発

プラグインはクラス ライブラリ ファイル (DLL) または実行可能ファイル (EXE) として実装できます。DLL シナリオでの手順は次のとおりです。

  1. 新しいクラス ライブラリ プロジェクトを作成します。
  2. WPF アセンブリの PresentationCore、PresentationFramework、System.Xaml、および WindowsBase を参照します。
  3. WpfHost.Interfaces アセンブリへの参照を追加します。[ローカルにコピー] が false に設定されていることを確認してください。
  4. 新しい WPF ユーザー コントロール (MainUserControl など) を作成します。
  5. IKriv.WpfHost.Interfaces.PluginBase から派生する Plugin というクラスを作成します。
  6. ホストの plugins.xml ファイルにプラグインのエントリを追加します。
  7. プラグインをコンパイルしてホストを実行します。

最も小さなプラグイン クラスは次のようになります。

public class Plugin : PluginBase
 {
   public override FrameworkElement CreateControl()
   {
     return new MainUserControl();
   }
 }

同様に、プラグインを実行可能ファイルとして実装することもできます。この場合の手順は次のとおりです。

  1. WPF アプリケーションを作成します。
  2. WPF ユーザー コントロール (MainUserControl など) を作成します。
  3. アプリケーションのメイン ウィンドウに MainUserControl を追加します。
  4. WpfHost.Interfaces アセンブリへの参照を追加します。[ローカルにコピー] が false に設定されていることを確認してください。
  5. IKriv.WpfHost.Interfaces.PluginBase から派生する Plugin というクラスを作成します。
  6. ホストの plugins.xml ファイルにプラグインのエントリを追加します。

次に示すようなプラグイン クラスが作成され、メイン ウィンドウの XMAL には MainUserControl への参照のみが含まれているはずです。

<Window x:Class="MyPlugin.MainWindow"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:MyProject"
  Title="My Plugin" Height="600" Width="766" >
  <Grid>
    <local:MainUserControl />
  </Grid>
</Window>

このように実装したプラグインは、スタンドアロン アプリケーションとして実行することも、ホスト内で実行することもできます。これにより、ホスト統合に関連しないプラグイン コードのデバッグは簡素化されます。こうした "デュアルヘッド" なプラグインのクラス ダイアグラムを図 6 に示します。


図 6 デュアルヘッドなプラグインのクラス ダイアグラム

この手法により、既存のアプリケーションを簡単にプラグインに変換することもできるようになります。必要なのはアプリケーションのメイン ウィンドウをユーザー コントロールに変換することだけです。後は、既にデモしたとおりにプラグイン クラスでそのユーザー コントロールのインスタンスを作成します。付属のコード ダウンロードの Solar System プラグインはこのような変換の例です。変換プロセスが完了するまでは 1 時間もかかりません。

プラグインは独立したアプリケーションでなく、ホストから起動されるため、デバッグは簡単にはいかないかもしれません。ホストのデバッグを開始することはできますが、Visual Studio で子プロセスを自動的にアタッチすることはまだできません。プラグイン プロセスの実行後デバッガーを手動でプロセスにアタッチするか、PluginProcess app.config の 4 行目を次のように変更し、デバッガーの起動時にプラグイン プロセスをデバッガーに割り込ませます。

代わりとなるもう 1 つの方法は、前述したようにプラグインをスタンドアロン アプリケーションとして作成することです。このようにすることで、プラグインの大部分をスタンドアロン アプリケーションとしてデバッグでき、WPF ホストとの統合に関する定期的なチェックのみが適切に動作します。

デバッガーの起動時にプラグイン プロセスをデバッガーに割り込ませる場合は、WpfHost app.config ファイルの 4 行目を次のように変更し、ready イベントのタイムアウト値を増やすことが必要です。

付属のコード ダウンロードで一連のプラグイン例を確認できます。各プラグインの内容に関する説明を図 7 に示します。

図 7 付属のコード ダウンロードでプラグインの例を確認できる

プラグイン プロジェクト 内容
BitnessCheck プラグインを 32 ビットまたは 64 ビットとして実行する方法をデモする
SolarSystem プラグインに変換された古い WPF デモ アプリケーションをデモする
TestExceptions ユーザー スレッドとワーカー スレッドで発生した例外の例外処理をデモする
UseLogServices ホスト サービスとプラグイン サービスの使用をデモする

ホスト サービスとプラグイン サービス

現実的には、プラグインでは多くの場合にホストから提供されるサービスを使用する必要があります。このシナリオは、コード ダウンロードの UseLogService プラグインでデモしています。プラグイン クラスでは、既定のコンストラクター、または IWpfHost 型のパラメーターを 1 つ受け取るコンストラクターを使用します。後者の場合は、プラグインのローダーによって WPF ホストのインスタンスがプラグインに渡されます。IWpfHost インターフェイスの定義は次のとおりです。

public interface IWpfHost : IServiceProvider
 {
   void ReportFatalError(string userMessage,
      string fullExceptionText);
   int HostProcessId { get; }
 }

このプラグインでは IServerProvider パーツを使用します。IServiceProvider は、mscorlib.dll で定義されている標準的な .NET Framework インターフェイスです。

public interface IServiceProvider
 {
   object GetService(Type serviceType);
 }

IServiceProvider はホストから ILog サービスを取得するために使用します。

class Plugin : PluginBase
 {
   private readonly ILog _log;
   private MainUserControl _control;
   public Plugin(IWpfHost host)
   {
     _log = host.GetService();
   }
   public override FrameworkElement CreateControl()
   {
     return new MainUserControl { Log = _log };
   }
 }

これにより、コントロールで ILog ホスト サービスを使用してホストのログ ファイルに書き込めるようになります。

ホストではプラグインから提供されるサービスを使用することもできます。今回は、このようなサービスとして実際に役に立つことが実証されている IUnsavedData サービスを定義しました。このインターフェイスを実装すると、プラグインでは一連の未保存の作業項目を明確にすることができます。プラグインまたはホスト アプリケーション全体が終了している場合は、未保存のデータを破棄するかどうかをたずねるメッセージをホストから表示します (図 8 参照)。


図 8 IUnsavedData サービスの使用

IUnsavedData インターフェイスの定義は次のとおりです。

public interface IUnsavedData
 {
   string[] GetNamesOfUnsavedItems();
 }

プラグインの作成者は、IServiceProvider インターフェイスを明示的に実装する必要はなく、プラグインに IUnsavedData インターフェイスを実装すれば十分です。IUnsavedData インターフェイスは PluginBase.GetService メソッドによってホストに返されます。コード ダウンロードの UseLogService プロジェクトに、以下に示す関連コードと共にサンプルの IUnsavedData 実装を用意しています。

class Plugin : PluginBase, IUnsavedData
 {
   private MainUserControl _control;
   public string[] GetNamesOfUnsavedItems()
   {
     if (_control == null) return null;
     return _control.GetNamesOfUnsavedItems();
   }
 }

ログ記録とエラー処理

WPF ホストとプラグイン プロセスは、%TMP%\WpfHost ディレクトリにログを作成します。WPF ホストは WpfHost.log に、各プラグイン ホスト プロセスは PluginProcess.Guid.log にログを書き込みます ("Guid" はリテラル名ではなく、実際の Guid 値が展開されます)。ログ サービスは独自に構築したものです。サンプルを自己完結型にするため、log4net や NLog といった一般的なログ サービスは使用しませんでした。

また、プラグイン プロセスではコンソール ウィンドウにも結果を書き込みます。このコンソール ウィンドウは WpfHost app.config ファイルの 3 行目を次のように変更することで表示できます。

ここでは細心の注意を払ってすべてのエラーをホストに報告し、適切に処理します。ホストはプラグイン プロセスを監視し、プラグイン プロセスが終了するとプラグイン ウィンドウを閉じます。同様に、プラグイン プロセスはホストを監視し、ホストが終了するとプラグイン プロセスを終了します。すべてのエラーがログ記録されるため、トラブルシューティングではログ ファイルの検証が非常に役立ちます。

ホストとプラグイン間でやり取りされるすべてのものが Serializable であるか、MarshalByRefObject から派生した型である必要があることに注意してください。これ以外の場合は、.NET リモート処理によって当事者間でオブジェクトをマーシャリングできなくなります。型とインターフェイスは両当事者が認識している必要があるため、一般的には組み込み型と、WpfHost.Interfaces アセンブリまたは PluginHosting アセンブリに格納されている型のみ安全にマーシャリングできます。

バージョン管理

WpfHost.exe、PluginProcess.exe、および PluginHosting.dll は密結合されているため、同時にリリースする必要があります。さいわいにもプラグイン コードではこれら 3 つのどのアセンブリも使用しないため、アセンブリはほぼどのような方法でも変更できます。たとえば、プラグインに影響を与えることなく同期メカニズムや ready イベントの名前を簡単に変更できます。

WpfHost.Interfaces.dll コンポーネントは、特に注意を払ってバージョン管理する必要があります。このコンポーネントは参照するものですが、プラグイン コード (CopyLocal=false) には含めません。したがって、このアセンブリのバイナリは常にホストからのみ提供されます。サイドバイサイド実行は特に望んでいないので、このアセンブリには厳密名を付けていません。システム全体で WpfHost.Interfaces.dll の 1 つのバージョンだけが表示されるようにします。

一般に、プラグインはホスト作成者の管理下にないサードパーティ製コードと考えてください。すべてのプラグインを一度に変更したり、再コンパイルしたりすることは困難または不可能です。そのため、新しいバージョンのインターフェイス アセンブリでは互換性を損なう変更を最小限に抑え、前バージョンとのバイナリ互換が可能であるようにすることが必要です。

通常、アセンブリに新しい型とインターフェイスを追加しても問題はありません。その他の変更 (インターフェイスに新しいメソッドを追加したり列挙型に新しい値を追加するなど) は、バイナリ互換ではなくなる可能性があるため、避けるようにします。

ホスト アセンブリに厳密名がなくても、バージョン番号が同じでコードが異なるアセンブリが 2 つできるようなことがないように、変更の大小にかかわらず、変更後はバージョン番号を増やすことが重要です。

優れた出発点

今回紹介した参照アーキテクチャは、プラグインとホストの統合に関して製品化できる品質のフレームワークとは呼べませんが、あと一歩のところまで来ており、アプリケーションの優れた出発点になります。

このアーキテクチャでは、とりわけプラグイン プロセスのライフサイクル、プロセス全体でのプラグイン コントロールのマーシャリング、交換のメカニズム、他サービスの中でのホストとプラグイン間のサービスの検出といった、典型的かつ難易度の高い考慮事項に対処しています。設計に関するソリューションや回避策のほとんどは恣意的なものではなく、WPF 向け複合アプリケーションの構築における実際の経験に基づいています。

必要になる可能性が最も高いのは、ホストの外観の変更、勤務先で使用する標準メカニズムへのログ記録メカニズムの置き換え、および新しいサービスの追加です。場合によってはプラグインの検出方法の変更も必要になります。他にも多数の変更や改良を加えることができます。

WPF 向け複合アプリケーションを作成しない方も、.NET Framework の機能性と柔軟性のデモや、生産性が高く、予想外の興味深い方法でなじみのあるコンポーネントを組み合わせる方法のデモとして、このアーキテクチャを検証することをお勧めします。

Ivan Krivyakov は、Thomson Reuters のテクニカル リードです。彼は、Windows Presentation Foundation の複雑な基幹業務 (LOB) アプリケーションの構築と改良を専門とする現役開発者です。

この記事のレビューに協力してくれた技術スタッフの Dr. James McCaffrey、Daniel Plaisted、および Kevin Ransom に心より感謝いたします。
Kevin Ransom は、マイクロソフトに 14 年間勤務しており、共通言語ランタイム、Microsoft Business Framework、Windows Vista と Windows 7、Managed Extensibility Framework、ベース クラス ライブラリなど、多数のプロジェクトに携わってきました。

Dr. James McCaffrey は、ワシントン州レドモンドにあるマイクロソフト本社に勤務しています。これまでに、Internet Explorer、MSN サーチなどの複数のマイクロソフト製品にも携わってきました。また、『.NET Test Automation Recipes: A Problem-Solution Approach』(Apress、2006 年) の著者でもあります。連絡先は jammc@microsoft.com(英語のみ) です。

Daniel Plaisted は、2008 年にマイクロソフトに入社して以来、Managed Extensibility Framework (MEF)、ポータブル クラス ライブラリ (PCL)、および Windows ストア アプリ用 Microsoft .NET Framework に取り組んできました。彼は、MS TechEd、BUILD など、さまざまなローカル グループ、コード キャンプ、および会議で発表してきました。余暇には、コンピューター ゲーム、読書、ハイキング、ジャグリング、およびフットバッグ (ハッキーサック) を楽しんでいます。彼のブログは、(blogs.msdn.com/b/dsplaisted/、英語) で、連絡先は daplaist@microsoft.com. (英語のみ) です。