MSDN マガジン > Home > 発行物 > 2008 > September >  Prism: WPF で複合アプリケーションを作成するためのパターン
Prism
WPF で複合アプリケーションを作成するためのパターン
Glenn Block

この記事では、次の内容について説明します。
  • 複合アプリケーションの基礎
  • ブートストラップとモジュールの初期化
  • 領域と RegionManager
  • ビュー、コマンド、およびイベント
この記事では、次のテクノロジを使用しています。
Composite Application Guidance for WPF
Windows ® Presentation Foundation (WPF) や Silverlight™ などのテクノロジは、ユーザー エクスペリエンスの充実したアプリケーションをすばやく簡単に配布する単純な宣言型の手段を開発者に提供します。しかし、これらのテクノロジはプレゼンテーション層を論理層から分離するのに役立ちますが、管理しやすいアプリケーションを作成するという昔ながらの問題は解決しません。
小さなプロジェクトでは、中程度の経験を有する開発者が管理しやすく拡張可能なアプリケーションをデザインおよび作成できると期待できます。しかし、可変部分の数、およびこれらの部分を操作する人の数が増えると、プロジェクトを制御下に置くことが急激に難しくなります。
複合アプリケーションは、この問題に対するソリューションです。この記事では、複合アプリケーションとは何か、また WPF の機能を利用する複合アプリケーションをどのようにして作成できるかについて説明します。さらに、マイクロソフトの patterns & practices チームによる新しい Composite Application Guidance for WPF (以前のコードネームは "Prism" でした) を紹介します。

問題 : モノリシック アプリケーション
複合アプリケーションの必要性を理解するための例について考えてみましょう。Contoso Financial Investments は、株式投資ポートフォリオを管理するためのアプリケーションを提供します。このアプリケーションを使用して、ユーザーは、現在の投資とそれらの投資に関連するニュース アイテムを表示すること、ウォッチ リストにアイテムを追加すること、および売買取引を実行することができます。
これをユーザー コントロールのある従来の WPF アプリケーションとして作成する場合は、まず最上位ウィンドウから開始し、前述の各機能のユーザー コントロールを追加します。この例には、PositionGrid、PositionSummary、TrendLine、WatchList などのユーザー コントロールがあります (図 1 を参照)。各ユーザー コントロールは、XAML で手動で、または Expression Blend™ などのデザイナを使用して、デザイン時にメイン ウィンドウ内にレイアウトされます。
図 1 モノリシック アプリケーションのユーザー コントロール (クリックすると拡大画像が表示されます)
次に、RoutedEvents、RoutedCommands、およびデータ バインドを使用してすべてを関連付けます。このトピックの詳細については、今月号に掲載されている Brian Noyes の「WPF におけるルーティング イベントとルーティング コマンドについて」という記事を参照してください (msdn.microsoft.com/magazine/cc785480)。PositionGrid には、選択用の RoutedCommand が関連付けられています。コマンドの Execute ハンドラで、ポジションが選択されるたびに TickerSymbolSelected イベントが発生します。TickerSymbolSelected イベントをリスニングし、選択したティッカー シンボルに基づいてコンテンツをレンダリングするために、TrendLine と NewsReader が関連付けられています。
この場合、アプリケーションは各コントロールと緊密に結合しています。UI には、さまざまな部分を調整するための十分な量のロジックがあります。コントロール間の相互依存関係もあります。
このような依存関係があるため、異なる各部分を個別に開発できる形式にアプリケーションを分割する簡単な方法がありません。すべてのユーザー コントロールを別々のアセンブリに配置して管理しやすさを向上させることはできますが、問題がメイン アプリケーションからコントロール アセンブリに移動するだけです。このモデルでは、大量の変更を行ったり新しい機能を導入したりすることが非常に困難です。
ここで、2 つの新しいビジネス要件を追加して、問題を複雑にしてみましょう。最初に、選択されたファンドがダブルクリックされたときに、そのファンドに関する個人的なメモを表示するファンド メモ画面を追加します。次に、選択したファンドに関連するハイパーリンクの一覧を表示する新しい画面を追加します。時間の制約により、これらの機能を異なるチームで並列に開発する必要があります。
各チームで、FundNotes および FundLinks という別々のコントロールを開発します。両方のコントロールを同じコントロール アセンブリに追加するには、これらをそれぞれコントロール プロジェクトに追加する必要があります。さらに重要なのは、これらをメイン フォームに追加する必要があることです。これは、各コントロールのコードと XAML の両方に対する変更をメイン フォームでマージする必要があることを意味します。この種の操作は、特に既存のアプリケーションの場合には、非常に不安定になります。
これらすべての変更がメイン アプリケーションに戻されたことをチェックするにはどうすればよいでしょうか。この作業が終わるまでに、ソース コントロールのマージと差異の確認におそらく多大な時間を費やすことになります。変更の適用でミスをした場合、または間違って何かを上書きしてしまった場合は、壊れたアプリケーションが残されます。対策は、アプリケーション デザインの再考にあります。

複合アプリケーション
複合アプリケーションは、動的に検出され実行時に構築される疎結合モジュールから構成されます。モジュールには、システムのさまざまな垂直スライスを表すビジュアル コンポーネントと非ビジュアル コンポーネントが含まれます (図 2 を参照)。ビジュアル コンポーネント (ビュー) は、すべてのアプリケーション コンテンツのホストとして機能する共通シェル内に構成されます。複合により、これらのモジュールレベル コンポーネントを関連付けるサービスが提供されます。モジュールは、アプリケーションの特定の機能に関連する追加サービスを提供できます。
図 2 複合アプリケーションのコンポーネント (クリックすると拡大画像が表示されます)
大まかにいうと、複合アプリケーションは、それ自身がビューである子を含むビューの再帰的 UI 構造を記述する複合ビュー デザイン パターンの実装です。ビューは、デザイン時に静的に構成されるのではなく、通常は実行時にメカニズムによって構成されます。
パターンの利点を示すために、複数の注文インスタンスがある受注システムについて考えます。各インスタンスは、ヘッダー、詳細、出荷、および受領を表示するために非常に複雑になることがあります。システムが進化すると、さらに多くの情報の表示が必要になります。注文の種類に応じて、注文の各部分を異なる方法で表示することも想像してください。
このような画面が静的に作成される場合は、最終的に、注文のさまざまな部分を表示するための条件付きロジックが大量に生じる可能性があります。また、新しい機能を追加すると、既存のロジックが壊れる可能性が増します。一方、これを複合ビューとして実装する場合は、関連する部分のみの注文画面を動的に構成します。このため、条件付き表示ロジックを廃止し、注文ビュー自体は変更せずに新しい子画面を追加できます。
モジュールは、メイン複合ビュー (シェルとも呼ばれます) の作成元のビューに寄与します。モジュールが相互に直接参照したり、シェルを直接参照したりすることはありません。代わりに、ユーザー アクションに応答するために、サービスを利用して相互に通信およびシェルと通信します。
システムをモジュールで構成することにはいくつかの利点があります。モジュールは、同じアプリケーション内の異なるバックエンド システムから取得されたデータを集約できます。また、システムは時間の経過と共により簡単に進化できます。システム要件が変化したときに、非モジュール システムよりもはるかに少ない手間で新しいモジュールをシステムに追加できます。既存のモジュールを独立して進化させることができるため、テストの容易性が向上します。最後に、モジュールは、異なるチームで開発、テスト、および保守できます。

Composite Application Guidance
Microsoft patterns & practices チームは、最近、Composite Application Guidance for WPF の最初のバージョンを出荷しました (microsoft.com/CompositeWPF で入手できます)。WPF の機能とプログラミング モデルを利用するために、新しいガイダンスがデザインされました。同時に、チームは内部の製品チーム、顧客、および .NET コミュニティからのフィードバックに基づいて、以前の複合アプリケーション ガイダンスのデザインを改善しました。
Composite Application Guidance for WPF には、参照実装 (前に説明した Stock Trader アプリケーション)、Composite Application Library (CAL)、クイックスタート アプリケーション、および設計文書と技術文書が含まれています。
CAL は、複合アプリケーションを作成するためのサービスと機能を提供します。各サービスを CAL デザイン アプリケーションの一部として 1 つずつまたはまとめて使用できる構成モデルを使用します。CAL を再コンパイルせずに、各サービスを簡単に置換することもできます。たとえば、CAL には依存関係注入に Unity Application Block を使用する拡張機能が付属していますが、独自の依存関係注入サービスでこれを置換することもできます。
クイックスタートは、さまざまな CAL コンポーネントの使用方法を示すことに焦点を絞った小さなアプリケーションを提供します。これらは、一度にすべてを把握する必要なく、概念を理解できるようにデザインされています。
この記事の残りの部分では、Stock Trader 参照実装で示されている複合の技術的な概念をいくつか検証します。記事のすべてのコードは、MSDN® (msdn.microsoft.com/library/cc707819) から Composite Application Guidance for WPF をダウンロードして入手できます。

ブートストラップとコンテナ
CAL を使用して複合アプリケーションを作成する場合、最初に複数のコア構成サービスを初期化する必要があります。ここで、ブートストラップが登場します。ブートストラップは、構成に必要なすべての機能 (図 3 に示します) を実行します。多くの点で、これは CAL アプリケーションの Main メソッドです。
図 3 ブートストラップ初期化タスク (クリックすると拡大画像が表示されます)
最初に、コンテナが初期化されます。コンテナは、制御の反転 (IoC: inversion of control)/依存関係の注入 (DI: dependency injection) コンテナを意味します。この用語を聞き慣れない場合は、James Kovacs による MSDN Magazine の記事「ソフトウェアの依存関係を緩和してアプリケーションの柔軟性を高める」(msdn.microsoft.com/magazine/cc337885) を参照してください。
コンテナは、CAL アプリケーションで重要な役割を果たします。コンテナは、構成に使用されるすべてのアプリケーション サービスのストアです。これは、必要な場合にこれらのサービスの注入を担当します。既定では、CAL には patterns & practices の Unity フレームワークをコンテナとして使用する抽象 UnityBootstrapper が含まれます。ただし、CAL は Windsor、Structure Map、Sprint.NET などの他のコンテナと連動するように作成されています。CAL 内のクラス (Unity 拡張機能以外) はいずれも特定のコンテナに依存しません。
コンテナが構成されるときに、構成に使用されるロガーやイベント アグリゲータなどのいくつかのコア サービスが自動的に登録され、ベースのブートストラップによってこれらをオーバーライドできます。たとえば、自動的に登録される 1 つのサービスに IModuleLoader があります。ブートストラップで ConfigureContainer メソッドをオーバーライドする場合は、独自のモジュール ローダーを登録できます。
protected override void ConfigureContainer() {
  Container.RegisterType<IModuleLoader, MyModuleLoader>();
  base.ConfigureContainer();
}
サービスを既定で登録しない場合は、これをオフにすることもできます。単純に、ブートストラップで Run メソッド オーバーロードを呼び出し、useDefaultConfiguration パラメータに false 値を渡します。
次に、領域アダプタが構成されます。領域は、モジュールが UIElement を注入できる、UI 内の指定された場所 (通常は、パネルなどのコンテナ) です。領域アダプタは、アクセスするさまざまな領域タイプの関連付けを処理します。これらのアダプタは、コンテナ内の RegionAdapterMappings シングルトン インスタンスにマップされています。
この時点で、シェルが作成されます。シェルは、領域が定義される最上位ウィンドウです。App.Xaml で宣言する代わりに、アプリケーション固有のブートストラップから CreateShell メソッドで作成します。これにより、シェルが表示される前にブートストラップの初期化が完了することが保証されます。
実際にはアプリケーションにシェルが必要でないと聞いて驚かれるかもしれません。たとえば、既存の WPF アプリケーションがあり、これにいくつかの CAL 機能を追加するとします。CAL コントロールを画面全体に表示する代わりに、最上位領域となるパネルを追加できます。この例では、シェルを定義する必要がありません。ブートストラップは、シェルが定義されていない場合にはその表示を単純に無視できます。

モジュールの初期化
最後に、モジュールが初期化されます。CAL アプリケーション内のモジュールは、別のアセンブリとして開発できる、複合アプリケーション内の分離単位ですが、別のアセンブリとして開発することは必須ではありません。CAL アプリケーションでは、モジュールに機能のほとんどが含まれます。
モジュールの読み込みは、IModuleEnumerator および IModuleLoader という 2 つのサービスが関係する 2 ステップのプロセスです。列挙子は、使用可能なモジュールの特定を担当します。これは、モジュールに関するメタデータを含む ModuleInfo オブジェクトのコレクションをいくつか返します。UnityBootstrapper には、適切な列挙子を返すためにオーバーライドする必要のある GetModuleEnumerator が含まれます。オーバーライドしないと、実行時に例外がスローされます。CAL には、ディレクトリ スキャンおよび構成からモジュールを静的に特定するための列挙子が含まれます。
読み込みの場合、CAL には既定で UnityBootstrapper によって使用される ModuleLoader が含まれます。これは、各モジュール アセンブリを読み込み (まだ読み込まれていない場合)、それらを初期化します。モジュールでは、他のモジュールに対する依存関係を指定できます。ModuleLoader は、依存関係ツリーを構築し、これらの仕様に基づいて適切な順序でモジュールを初期化します。

ブートストラップを使用する
UnityBootstrapper は抽象クラスであるため、StockTraderRIBootstrapper がこれをオーバーライドします (図 4 を参照)。ブートストラップには、独自のアプリケーション固有機能を挿入できる保護された仮想メソッドがいくつかあります。
public class StockTraderRIBootstrapper : UnityBootstrapper {
  private readonly EntLibLoggerAdapter _logger = new EntLibLoggerAdapter();

  protected override IModuleEnumerator GetModuleEnumerator()  {
    return new StaticModuleEnumerator()
    .AddModule(typeof(NewsModule))
    .AddModule(typeof(MarketModule))
    .AddModule(typeof(WatchModule), "MarketModule")
    .AddModule(typeof(PositionModule), "MarketModule", "NewsModule");
  }

  protected override ILoggerFacade LoggerFacade  {
    get { return _logger; }
  }

  protected override void ConfigureContainer()  {
    Container.RegisterType<IShellView, Shell>();

    base.ConfigureContainer();
  }

  protected override DependencyObject CreateShell()  {
    ShellPresenter presenter = Container.Resolve<ShellPresenter>();
    IShellView view = presenter.View;
    view.ShowView();
    return view as DependencyObject;
  }
}
最初に注目すべき点は、EntlibLoggerAdapter が定義され、_logger 変数に格納されていることです。コードは、LoggerFacade プロパティをオーバーライドしてこのロガーを返し、これで ILoggerFacade を実装します。この場合は、Enterprise Library のロガーを使用していますが、独自のアダプタを使用するように簡単に置換できます。
次に、GetModuleEnumerator メソッドが StaticModuleEnumerator を返すようにオーバーライドされます。StaticModuleEnumerator は、4 つの参照実装モジュールで事前に設定されています。参照実装では、静的モジュール読み込みを使用しますが、ディレクトリ ルックアップや構成など、モジュールを列挙する方法が他にもいくつかあります。別の列挙方法を使用するには、別の列挙子をインスタンス化するようにこのメソッドを変更するだけです。
次に、ConfigureContainer がシェルを登録するようにオーバーライドされます。必要に応じて、この時点で追加のサービスをプログラムで登録することもできます。最後に、CreateShell がシェルを作成する特定のロジックでオーバーライドされます。この場合は、コードが Model View Presenter パターンを実装しているため、シェルにはプレゼンタが関連付けられます。
図 4 に示すブートストラップは、CAL アプリケーションを最初から作成する場合の一般的なパターンを示しています。これは、アプリケーション固有のブートストラップを作成するものです。このアプローチの大きな利点は、アプリケーション固有のブートストラップによって、アプリケーションのテスト容易性が強化されることです。ブートストラップには、DependencyObject 以外の WPF に対する依存性がありません。たとえば、アプリケーション固有のブートストラップから継承するテスト ブートストラップを作成し、CreateContainer メソッドをオーバーライドして AutoMocking コンテナを返し、すべてのサービスをモック作成します。
また、ブートストラップは構成を初期化するための単一エントリ ポイントを提供し、CAL はアプリケーションのフレームワーク クラスからの継承に依存しないため、以前のフレームワークよりも少ない手間で CAL を既存のアプリケーションに統合できます。CAL 自体はブートストラップにまったく依存しないため、ブートストラップがニーズに合わない場合は、これを使用しないこともできます。

モジュールとサービス
前に述べたように、複合アプリケーションは CAL を使用して作成され、アプリケーション ロジックの大部分がモジュール内に存在します。Stock Trader 参照実装には、4 つのモジュールが含まれます。
  • NewsModule は、選択された各ファンドの関連ニュース フィードを提供します。
  • MarketModule は、選択されたファンドのトレンド データに加えてリアルタイム マーケット データを提供します。
  • WatchModule は、監視しているファンドの一覧を表示するウォッチ リストを提供します。
  • PositionModule は、投資したファンドの一覧を表示し、売買取引を実行できるようにします。
CAL では、モジュールは IModule インターフェイスを実装するクラスです。このインターフェイスには、Initialize という 1 つのメソッドしかありません。ブートストラップがアプリケーションの Main メソッドに相当する場合、Initialize メソッドは各モジュールの Main です。たとえば、WatchModule の Initialize メソッドを次に示します。
public void Initialize() {
  RegisterViewsAndServices();

  IWatchListPresentationModel watchListPresentationModel = 
    _container.Resolve<IWatchListPresentationModel>();
  _regionManager.Regions["WatchRegion"].Add(watchListPresentationModel.View);
  IAddWatchPresenter addWatchPresenter = 
    _container.Resolve<IAddWatchPresenter>();
  _regionManager.Regions["MainToolbarRegion"].Add(addWatchPresenter.View);
}
モジュールの詳細に入る前に、_container および _regionManager への参照という 2 つの項目について説明しておいたほうがよいでしょう。インターフェイスにこれらが定義されていない場合、これらはどこから来るのでしょうか。モジュールにロジックをハードコーディングして、これらの依存関係を見つけるのでしょうか。
さいわい、最後の質問に対する答えは「いいえ」です。IoC コンテナがここで役立ちます。モジュールが読み込まれると、コンテナから解決され、指定された依存関係もモジュールのコンストラクタに注入されます。
public WatchModule(IUnityContainer container, 
  IRegionManager regionManager) {
  _container = container;
  _regionManager = regionManager;
}
ここで、コンテナ自体がモジュールに注入されることがわかります。これが可能なのは、ブートストラップがコンテナをその ConfigureContainer メソッドで登録するためです。
Container.RegisterInstance<IUnityContainer>(Container);
コンテナに直接アクセスするモジュールによって、モジュールはコンテナからの依存関係を強制的に登録および解決できます。
この強制登録は必須ではありません。代わりに、すべてのサービスをグローバル構成に配置できます。これは、コンテナが最初に作成されたときにすべてのサービスが登録される必要があることを意味します。ただし、ほとんどのモジュールにはモジュール固有のサービスがあります。また、登録をモジュールに保持することで、これらのモジュール固有サービスは、モジュールが読み込まれた場合にのみ登録されます。
前に示したモジュールの場合、最初の呼び出しは RegisterViewsAndServices に対するものです。このメソッドでは、WatchModule の特定のビューがそれぞれインターフェイスと共にコンテナに登録されます。
protected void RegisterViewsAndServices() {
  _container.RegisterType<IWatchListService, WatchListService>(
    new ContainerControlledLifetimeManager());
  _container.RegisterType<IWatchListView, WatchListView>();
  _container.RegisterType<IWatchListPresentationModel, 
    WatchListPresentationModel>();
  _container.RegisterType<IAddWatchView, AddWatchView>();
  _container.RegisterType<IAddWatchPresenter, AddWatchPresenter>();
}
インターフェイスが指定されることを要求すると、考慮事項の分離が促進され、システムの他のモジュールが直接参照を必要とせずにビューと対話できます。すべてをコンテナに配置すると、さまざまなオブジェクトの各依存関係を自動的に注入できます。たとえば、WatchListView はコードに直接インスタンス化されず、代わりに WatchListPresentationModel コンストラクタの依存関係として読み込まれます。
public WatchListPresentationModel(IWatchListView view...)
ビューに加えて、WatchModule は WatchListService を登録します。WatchListService はリスト データを含み、新しいアイテムの追加に使用されます。登録される特定のビューは、ウォッチ リストとウォッチ リスト ツール バーです。登録後、領域マネージャが使用され、登録された両方のビューが WatchRegion と ToolbarRegion に追加されます。

領域と RegionManager
モジュール自体は、UI にコンテンツをレンダリングできない限り、特に興味深いものではありません。前のセクションでは、Watch モジュールで領域を使用して 2 つのビューを追加しました。領域を使用すると、モジュールが UI への特定の参照を保持し、注入されたビューをレイアウトおよび表示する方法を認識する必要がなくなります。この例として、図 5 に、WatchModule が注入する領域を示します。
図 5 アプリケーションにモジュールを注入する (クリックすると拡大画像が表示されます)
CAL には、基本的にこれらの場所をラップするハンドルである Region クラスが含まれます。Region クラスには、領域内に表示するビューの読み取り専用コレクションである Views プロパティが含まれます。ビューは、領域の追加メソッドを呼び出すことで領域に追加されます。Views プロパティには、実際にはオブジェクトの汎用コレクションが含まれます。UIElement のみを含むことには限定されません。このコレクションは、INotifyPropertyCollectionChanged を実装するため、領域に関連付けられている UIElement をこれにバインドし、変化を観察できます。
Views コレクションが UIElement 型ではなく、柔軟に型指定されているのはなぜかと疑問に思うかもしれません。WPF の豊富なテンプレート サポートのおかげで、実際にモジュールを領域に直接追加できます。そのモデルには、モデルのレンダリングを定義する関連 DataTemplate を定義できます。追加されたアイテムが UIElement またはユーザー コントロールの場合、WPF はそのままレンダリングします。このため、未処理注文のタブである領域がある場合は、OrderModel または OrderPresentationModel を単純に領域に追加してから、カスタム OrderView ユーザー コントロールを作成する代わりにカスタム DataTemplate を定義して表示を制御できます。
領域は、2 とおりの方法で登録できます。最初の方法は、UIElement に RegionName 添付プロパティで注釈を付けることにより XAML で定義されます。たとえば、MainToolbarRegion を定義する XAML は次のようになります。
<ItemsControl Grid.Row="1" Grid.Column="1" 
  x:Name="MainToolbar" 
  cal:RegionManager.RegionName="MainToolbarRegion">
  <ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel />
    </ItemsPanelTemplate>
  </ItemsControl.ItemsPanel>
</ItemsControl>
領域は、XAML を介して定義された後、実行時に RegionManager で自動的に登録されます。RegionManager は、ブートストラップによって登録された構成サービスの 1 つです。RegionManager は、本質的に、キーが領域の名前で値が IRegion インターフェイスのインスタンスである辞書です。RegionManager 添付プロパティでは、RegionAdapter を使用してこのインスタンスを作成します。
ただし、添付プロパティの使用が機能しないか、追加の領域を動的に登録する必要がある場合は、Region クラスまたは派生クラスのインスタンスを手動で作成し、RegionManager の Regions コレクションに追加できます。
XAML のコードでは、MainToolbarRegion が ItemsControl であることに注目してください。CAL には、ブートストラップによって登録される ContentControlRegionAdapter、ItemsControlRegionAdapter、SelectorRegionAdapter という 3 つの領域アダプタが付属しています。アダプタは、RegionAdapterMappings クラスで登録されます。アダプタはすべて、IRegionAdapter インターフェイスを実装する RegionAdapterBase から継承します。
図 6 に、ItemsControlRegionAdapter の実装を示します。アダプタ自体がどのように実装されるかは、採用される UIElement の種類に全面的に依存します。ItemsControlRegionAdapter の場合は、実装の大部分が Adapt メソッド内にあります。Adapt メソッドは 2 つのパラメータを受け取ります。最初のパラメータは、RegionManager が作成する Region クラス自体のインスタンスです。2 番目のパラメータは、領域を表す UIElement です。Adapt メソッドは、領域が要素を処理することを確認するために、関連機能を実行します。
public class ItemsControlRegionAdapter : RegionAdapterBase<ItemsControl> {
  protected override void Adapt(IRegion region, ItemsControl regionTarget) {
    if (regionTarget.ItemsSource != null || 
      (BindingOperations.GetBinding(regionTarget, 
      ItemsControl.ItemsSourceProperty) != null))
      throw new InvalidOperationException(
        Resources.ItemsControlHasItemsSourceException);

    if (regionTarget.Items.Count > 0) {
      foreach (object childItem in regionTarget.Items) {
        region.Add(childItem);
      }
      regionTarget.Items.Clear();
    }
    regionTarget.ItemsSource = region.Views;
  }

  protected override IRegion CreateRegion() {
    return new AllActiveRegion();
  }
}
ItemsControl の場合、アダプタは ItemControl 自体から子アイテムを自動的に削除してから、それらを領域に追加します。領域の Views コレクションは、コントロールの ItemsSource にバインドされます。
オーバーライドされている 2 つ目のメソッドは、新しい AllActiveRegion インスタンスを返す CreateRegion です。領域には、アクティブまたは非アクティブなビューを含めることができます。ItemsControl の場合は、選択の概念がないため、すべてのアイテムが常にアクティブです。一方、Selector などの他の種類の領域の場合は、一度に 1 つのアイテムのみ選択されます。ビューでは、選択されたことが領域によって通知されるように IActiveAware インターフェイスを実装できます。ビューが選択されるたびに、その IsSelected プロパティが true に設定されます。
複合アプリケーションの開発全体を通して、サードパーティ ベンダのコントロールに適応するものなど、追加の領域および領域アダプタを作成する必要があります。新しい領域アダプタを登録するには、ブートストラップで ConfigureRegionAdapterMappings メソッドをオーバーライドします。その後で、次のようなコードを追加します。
protected override RegionAdapterMappings 
  ConfigureRegionAdapterMappings() {
  RegionAdapterMappings regionAdapterMappings = 
    base.ConfigureRegionAdapterMappings();

  regionAdapterMappings.RegisterMapping(typeof(Selector), 
    new MyWizBangRegionAdapter());

  return regionAdapterMappings;
}
定義された領域には、RegionManager サービスを捕らえておくことでアプリケーション内の任意のクラスからアクセスできます。CAL アプリケーションでこの操作を行う一般的な方法は、依存関係注入コンテナで、RegionManager を必要とするクラスのコンストラクタに RegionManager を注入することです。領域にビューまたはモデルを追加するには、単純に領域の Add メソッドを呼び出します。ビューを追加する場合は、オプションで名前を渡すことができます。
_regionManager.Regions["MainRegion"].Add(
  somePresentationModel, "SomeView");
後でこの名前を使用して、領域の GetView メソッドを使用して領域からビューを取得できます。

ローカルに範囲指定された領域
既定では、アプリケーションには 1 つの RegionManager インスタンスしかないため、すべての領域がグローバルに範囲指定されます。これにより広いシナリオのセットが扱われますが、特定の範囲にのみ存在する領域を定義することが必要な場合があります。この操作を行う必要がある例は、ビューの複数のインスタンスを同時に表示できる従業員詳細のビューがアプリケーションに存在する場合です。これらのビューがかなり複雑な場合、ビューはミニシェルや CompositeView のように動作します。その場合は、シェルと同じように、各ビューに固有の領域が必要です。CAL により、ビューのローカル RegionManager を定義できるため、そのビュー内またはその子ビュー内で定義されている領域は、そのローカル領域内に自動的に登録されます。
UI Composition クイックスタートには、この従業員シナリオを示すガイダンスが含まれていました (図 7 を参照)。クイックスタートには、従業員リストがあります。各従業員をクリックすると、関連する従業員詳細が表示されます。従業員を選択するたびに、新しい EmployeeDetailsView がその従業員に対して作成され、DetailsRegion に追加されます (図 8 を参照)。このビューには、ローカル TabRegion が含まれます。EmployeesController がその OnEmployeeSelected に ProjectListView を注入します。
図 7 RegionManager を介した UI Composition (クリックすると拡大画像が表示されます)
public virtual void 
  OnEmployeeSelected(BusinessEntities.Employee employee) {
  IRegion detailsRegion = 
    regionManager.Regions[RegionNames.DetailsRegion];
  object existingView = detailsRegion.GetView(
    employee.EmployeeId.ToString(CultureInfo.InvariantCulture));

  if (existingView == null) {
    IProjectsListPresenter projectsListPresenter = 
    this.container.Resolve<IProjectsListPresenter>();
    projectsListPresenter.SetProjects(employee.EmployeeId);

    IEmployeesDetailsPresenter detailsPresenter = 
      this.container.Resolve<IEmployeesDetailsPresenter>();
    detailsPresenter.SetSelectedEmployee(employee);

    IRegionManager detailsRegionManager = 
      detailsRegion.Add(detailsPresenter.View,
      employee.EmployeeId.ToString(CultureInfo.InvariantCulture), true);

    IRegion region = detailsRegionManager.Regions[RegionNames.TabRegion];
    region.Add(projectsListPresenter.View, "CurrentProjectsView");
    detailsRegion.Activate(detailsPresenter.View);
  }
  else {
    detailsRegion.Activate(existingView);
  }
}
領域は、TabControl としてレンダリングされ、静的コンテンツと動的コンテンツの両方を含みます。[General] タブと [Location] タブは、XAML 内で静的に定義されます。ただし、[Current Projects] タブにはビューが注入されています。
コードでは、新しい RegionManager インスタンスが detailsRegion.Add メソッドから返されています。また、ビューの名前を渡し、createRegionManagerScope パラメータを true に設定する Add のオーバーロードを使用していることにも注目してください。これにより、子で定義されている領域に使用されるローカル RegionManager インスタンスが作成されます。TabRegion 自体は、EmployeeDetailsView の XAML で定義されます。
<TabControl AutomationProperties.AutomationId="DetailsTabControl" 
  cal:RegionManager.RegionName="{x:Static local:RegionNames.TabRegion}" .../>
ローカル領域を使用すると、インスタンス領域を使用していない場合でも追加の利点があります。最上位境界の定義にこれらを使用して、モジュールがその領域を他の部分に自動的に公開しないようにすることができます。そのために必要なのは、モジュールの最上位ビューを領域に追加し、固有の範囲を持つように指定することだけです。そうすると、モジュールの領域が他の部分から効果的に保護されます。これらにアクセスすることは不可能ではありませんが、はるかに難しくなります。

ビューがない場合は、複合が不要になります。ビューは、アプリケーションが提供する機能へのゲートウェイとなるため、複合アプリケーション内で作成する最重要要素です。
通常、ビューはアプリケーションの画面です。ビューには他のビューを含めることができ、その場合は複合ビューになります。ビューの別の用途は、メニューとツール バーです。たとえば、Stock Trader の OrdersToolbar は、[Submit]、[Cancel]、[Submit All]、[Cancel All] の各ボタンを含むビューです。
WPF では、Windows フォームの世界での慣例よりもはるかに豊富なビューの概念がサポートされます。Windows フォームでは、基本的にコントロールは視覚表現としての使用に制限されていました。WPF では、このモデルもサポートされますが、さまざまな画面を表示するためにカスタム ユーザー コントロールを作成することもできます。Stock Trader アプリケーション全体を見た場合、これはビューの定義に採用されている主要なメカニズムです。
もう 1 つのアプローチは、モデルを使用することです。WPF では、任意のモデルを UI にバインドしてから、DataTemplate を使用してそれをレンダリングできます。テンプレートは再帰的にレンダリングされるため、テンプレートがモデルのプロパティにバインドされる要素をレンダリングする場合、そのプロパティはテンプレートを使用してレンダリングされます (使用可能な場合)。
この動作の例として、次のコード サンプルを見てみましょう。このサンプルは、Composition クイックスタートと同じ UI を実装しますが、もっぱらモデルと DataTemplate を使用します。プロジェクト全体は 1 つのユーザー コントロールではありません。図 9 に、EmployeeDetailsView の処理方法を示します。ビューは、ResourceDictionary で定義された 3 つの DataTemplate のセットとなっています。すべてが EmployeeDetailsPresentationModel から開始します。そのテンプレートは、TabControl としてレンダリングされる必要があることを宣言します。テンプレートの一部として、TabControl の ItemsSource を EmployeeDetailsPresentationModel の EmployeeDetails コレクション プロパティにバインドします。このコレクションには、従業員詳細が構築されるときに 2 つの情報が設定されます。
public EmployeesDetailsPresentationModel() {
  EmployeeDetails = new ObservableCollection<object>();
  EmployeeDetails.Insert(0, new HeaderedEmployeeData());
  EmployeeDetails.Insert(1, new EmployeeAddressMapUrl());
  ...
}
<ResourceDictionary 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:EmployeesDetailsView=
    "clr-namespace:ViewModelComposition.Modules.Employees.Views.EmployeesDetailsView">

  <DataTemplate 
    DataType="{x:Type EmployeesDetailsView:HeaderedEmployeeData}">
    <Grid x:Name="GeneralGrid">
      <Grid.ColumnDefinitions>
        <ColumnDefinition></ColumnDefinition>
        <ColumnDefinition Width="5"></ColumnDefinition>
        <ColumnDefinition Width="*"></ColumnDefinition>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <TextBlock Text="First Name:" Grid.Column="0" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Last Name:" Grid.Column="2" Grid.Row="0">
      </TextBlock>
      <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="2"></TextBlock>
      <TextBlock Text="Email:" Grid.Column="2" Grid.Row="2"></TextBlock>
      <TextBox x:Name="FirstNameTextBox" 
        Text="{Binding Path=Employee.FirstName}" 
        Grid.Column="0" Grid.Row="1"></TextBox>
      <TextBox x:Name="LastNameTextBox" 
        Text="{Binding Path=Employee.LastName}" 
        Grid.Column="2" Grid.Row="1"></TextBox>
      <TextBox x:Name="PhoneTextBox" Text="{Binding Path=Employee.Phone}" 
        Grid.Column="0" Grid.Row="3"></TextBox>
      <TextBox x:Name="EmailTextBox" Text="{Binding Path=Employee.Email}" 
        Grid.Column="2" Grid.Row="3"></TextBox>
    </Grid>
  </DataTemplate>

  <DataTemplate 
    DataType="{x:Type EmployeesDetailsView:EmployeeAddressMapUrl}">
    <Frame Source="{Binding AddressMapUrl}" Height="300"></Frame>
  </DataTemplate>

  <DataTemplate DataType="{x:Type
    EmployeesDetailsView:EmployeesDetailsPresentationModel}">
    <TabControl x:Name="DetailsTabControl" 
      ItemsSource="{Binding EmployeeDetails}" >
      <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}" 
          BasedOn="{StaticResource RoundedTabItem}">
          <Setter Property="Header" Value="{Binding HeaderInfo}" />
        </Style>
      </TabControl.ItemContainerStyle>
    </TabControl>
  </DataTemplate>
</ResourceDictionary>
コレクション内のアイテムごとに別々のタブがレンダリングされます。最初のアイテムがレンダリングされるときに、WPF は HeaderedEmployeeData に対して指定された DataTemplate を使用します。HeaderedEmployeeData モデルには、従業員名と連絡先情報が含まれます。関連テンプレートは、情報を表示するための一連のラベルとしてモデルをレンダリングします。2 つ目のアイテムは、EmployeeAddressMapUrl に対して指定されたテンプレートを使用してレンダリングされます。この場合は、従業員が居住している場所の地図が表示された Web ページを含むフレームをレンダリングします。
モデルとその関連テンプレートの組み合わせを通じて、あらかじめ知っていたビューのみが実行時に実際に存在するという点で、これはかなり大きなパラダイム シフトです。(Stock Trader で示されているように) 両方のアプローチのハイブリッドを実装することもできます。ハイブリッドでは、テンプレートを通じてレンダリングされるモデルにバインドされるコントロールを含むユーザー コントロールがあります。

分離されたプレゼンテーション
この記事で述べたように、複合を作成する利点の 1 つは、コードが管理およびテストしやすくなることです。これを実現するためにビュー内で適用できるプレゼンテーション パターンがいくつか確立されています。Composite Application Guidance for WPF 全体を通じて、UI で使用される 2 つの再帰パターンがあります。それは、Presentation Model と Supervising Controller です。
Presentation Model パターンでは、UI の動作とデータの両方を含むモデルを想定します。ビューは、プレゼンテーション モデルの状態を "ガラスに" 投影します。
背後では、モデルはビジネス モデルおよびドメイン モデルと対話します。モデルには、選択したアイテムや、要素がチェックされているかどうかなど、追加の状態情報も含まれます。ビューは Presentation Model に直接バインドし、これをレンダリングします (図 10 を参照)。WPF でのデータ バインド、テンプレート、およびコマンドの豊富なサポートにより、Presentation Model パターンは魅力的な開発オプションになります。
図 10 Presentation Model パターン (クリックすると拡大画像が表示されます)
Stock Trader アプリケーションでは、位置サマリーなどで Presentation Model を慎重に使用しています。
public class PositionSummaryPresentationModel :
  IPositionSummaryPresentationModel, INotifyPropertyChanged {
  public PositionSummaryPresentationModel(
    IPositionSummaryView view,...) {
    ...
  }

  public IPositionSummaryView View { get; set; }
  public ObservableCollection<PositionSummaryItem> 
    PositionSummaryItems { 
    get; set; }
}
PositionSummaryPresentationModel が INotifyPropertyChanged を実装してビューに変更を通知していることがわかります。PositionSummaryPresentationModel はコンテナから解決されるため、ビュー自体はその IPositionSummaryView インターフェイスを通じてコンストラクタに注入されます。このインターフェイスにより、ビューを単体テストでモック作成できます。Presentation Model は、識別可能な PositionSummaryItem のコレクションを公開します。これらのアイテムは、PostionSummaryView にバインドされてレンダリングされます。
Supervising Controller パターンには、モデル、ビュー、およびプレゼンタが存在します。これを図 11 に示します。モデルはデータであり、たいていはビジネス オブジェクトです。ビューは、モデルが直接バインドする UIElement です。最後に、プレゼンタは UI ロジックを含むクラスです。このパターンでは、ビューにはプレゼンタに委任してプレゼンタからのコールバックに応答し、コントロールの表示や非表示などの単純なアクションを実行する以外のロジックはほとんど含まれていません。
図 11 Supervising Controller パターン (クリックすると拡大画像が表示されます)
Presentation Model を優先している Stock Trader アプリケーションでも、Supervising Controller パターンがいくつかのインスタンスで使用されています。1 つの例は、トレンド ラインです (図 12 を参照)。PositionSummaryPresentationModel と同様に、TrendLinePresenter には ItrendLineView インターフェイスを通じて TrendLineView が注入されています。プレゼンタは、委任ロジックを通じてビューが呼び出す OnTickerSymbolSelected メソッドを公開します。このメソッドでは、プレゼンタはビューにコールバックして、その UpdateLineChart メソッドと SetChartTitle メソッドを呼び出します。
public class TrendLinePresenter : ITrendLinePresenter {
  IMarketHistoryService _marketHistoryService;

  public TrendLinePresenter(ITrendLineView view, 
    IMarketHistoryService marketHistoryService) {
    this.View = view;
    this._marketHistoryService = marketHistoryService;
  }

  public ITrendLineView View { get; set; }

  public void OnTickerSymbolSelected(string tickerSymbol) {
    MarketHistoryCollection historyCollection = 
      _marketHistoryService.GetPriceHistory(tickerSymbol);
    View.UpdateLineChart(historyCollection);
    View.SetChartTitle(tickerSymbol);
  }
}

分離されたプレゼンテーションを実装するときの課題の 1 つは、ビューとプレゼンテーション モデルまたはプレゼンタ間の通信です。これを処理するアプローチはいくつかあります。よく実装されるアプローチでは、プレゼンテーション モデルまたはプレゼンタに対するイベントを直接呼び出すかまたは発生させるイベント ハンドラをビューに作成します。プレゼンタへの呼び出しを開始する同じ UIElement は、状態変化または権限に基づいて UI で有効または無効にする必要があります。そのためには、これらの要素を無効にするためにコールバックに使用できるメソッドがビューに必要です。
もう 1 つのアプローチは、WPF コマンドを使用することです。コマンドは、相互の委任ロジックを必要とすることなく、これらの状況に明確に対処する方法を提供します。WPF 内の要素は、コマンドにバインドして、実行ロジックと要素の有効化または無効化の両方を処理できます。UIElement は、コマンドにバインドされると、コマンドの CanExecute プロパティが false の場合は常に自動的に無効になります。コマンドは、XAML で宣言によってバインドできます。
WPF には、すぐに使用できる RoutedUICommands が用意されています。これらのコマンドを使用するには、ビューの分離コード内に Execute メソッドと CanExecute メソッドのハンドラが必要です。これは、相互通信を行うにはコード変更が必要であることを意味します。RoutedUICommands には、WPF の論理ツリーにレシーバが必要であるなど、その他の制約もあります。この制約は、複合の作成で問題になる場合があります。
さいわい、RoutedUICommands は、コマンドの実装の 1 つにすぎません。WPF は ICommand インターフェイスを提供し、これを実装する任意のコマンドにバインドします。このため、あらゆるニーズを満たすカスタム コマンドを作成でき、分離コードに触れる必要はありません。欠点は、SaveCommand、SubmitCommand、CancelCommand などのカスタム コマンドをあらゆる場所で実装する必要があることです。
CAL には、Execute メソッドと CanExecute メソッドの 2 つの委任をコンストラクタで指定できる DelegateCommand<T> など、新しいコマンドが含まれています。このコマンドを使用すると、ビュー自体に定義されているメソッドを通じて委任する必要や、アクションごとにカスタム コマンドを作成する必要なしに、ビューを関連付けることができます。
Stock Trader アプリケーションでは、ウォッチ リストなどのいくつかの場所で DelegateCommand を使用しています。WatchListService は、ウォッチ リストにアイテムを追加するためにこのコマンドを使用します。
public WatchListService(IMarketFeedService marketFeedService) {
  this.marketFeedService = marketFeedService;
  WatchItems = new ObservableCollection<string>();
  AddWatchCommand = new DelegateCommand<string>(AddWatch);
}

ビューとプレゼンタ、またはビューとプレゼンテーション モデル間のコマンドのルーティングに加えて、イベント発行など、複合での処理が必要な他の種類の通信もあります。これらの場合は、発行元がサブスクライバから完全に分離されます。たとえば、モジュールは、サーバーからの通知を受信する Web サービス エンドポイントを公開できます。通知が受信された後で、同じモジュールまたは他のモジュール内のコンポーネントがサブスクライブできるイベントを生成する必要があります。
この機能をサポートするために、CAL には、コンテナに登録される EventAggregator サービスがあります。Event Aggregator パターンの実装であるこのサービスを使用すると、発行元とサブスクライバが疎結合形式で通信できます。EventAggregator サービスには、抽象 EventBase クラスのインスタンスであるイベントのリポジトリが含まれます。サービスには、イベント インスタンスを取得するための 1 つの GetEvent<TEventType> メソッドがあります。
CAL には、EventBase を継承し、WPF の特定のサポートを提供する CompositeWPFEvent<TPayload> クラスがあります。このクラスは、完全な .NET イベントではなく委任を使用して発行を行います。実際には、弱い委任として機能する DelegateReference クラスを既定で使用します (弱い委任の詳細については、msdn.microsoft.com/library/ms404247 を参照してください)。これにより、サブスクライバでは、明示的にサブスクライブ解除しない場合でもガベージ コレクションを行うことができます。
CompositeWPFEvent クラスには、Publish、Subscribe、および Unsubscribe メソッドが含まれます。それぞれ、汎用的な種類のイベント情報を使用して、発行元が正しいパラメータを渡し (TPayload)、Subscriber プロパティがそれらを受け取る (Action<TPayload>) ことを確認します。Subscribe メソッドでは、PublisherThread、UIThread、または BackgroundThread に設定できる ThreadOption を渡すことができます。このオプションによって、サブスクライブの委任が呼び出されるスレッドを決定できます。また、Subscribe メソッドがオーバーロードされ、Predicate<T> フィルタを渡せるようになるため、サブスクライバはフィルタが要件を満たすときにのみイベントの通知を受け取ります。
Stock Trader アプリケーションでは、EventAggregator はポジション画面でシンボルが選択されているかどうかを選択されるたびにブロードキャストに使用されます。News モジュールは、このイベントをサブスクライブし、そのファンドのニュースを表示します。次に、機能の実装方法を示します。
public class TickerSymbolSelectedEvent : 
  CompositeWpfEvent<string> {
}
最初に、StockTraderRI.Infrastructure アセンブリでイベントが定義されます。これは、すべてのモジュールが参照する共有アセンブリです。
public void Run() {
  this.regionManager.Regions["NewsRegion"].Add(
    articlePresentationModel.View);
  eventAggregator.GetEvent<TickerSymbolSelectedEvent>().Subscribe( 
    ShowNews, ThreadOption.UIThread);
}

public void ShowNews(string companySymbol) {
  articlePresentationModel.SetTickerSymbol(companySymbol);
}
News モジュールの NewsController は、Run メソッドでこのイベントをサブスクライブします。
private void View_TickerSymbolSelected(object sender, 
  DataEventArgs<string> e) {
  _trendLinePresenter.OnTickerSymbolSelected(e.Value);

  EventAggregator.GetEvent<TickerSymbolSelectedEvent>().Publish(
    e.Value);
}
PositionSummaryPresentation モデルは、シンボルが選択されるたびにイベントを生成します。

まとめ
ガイダンスは、microsoft.com/compositewpf からダウンロードしてください。コードを実行するために必要なのは、.NET Framework 3.5 がインストールされていることだけです。
ガイダンスには、作業の開始に役立つツールが含まれています。クイックスタートは、複合アプリケーションの作成のさまざまな側面に焦点を当てた理解しやすいサンプルを提供します。参照実装では、さまざまな側面をすべて利用する包括的な例を提供しています。最後に、ドキュメントには、背景情報、特定のタスクに関する完全な操作方法のセット、およびハンズオン ラボが記載されています。
ガイダンスを使用するときに、ご意見を CodePlex フォーラムに投稿するか、cafbk@microsoft.com に電子メールでお送りください。
Glenn Block は、.NET Framework 4.0 の新しい Managed Extensibility Framework (MEF) の PM です。MEF に取り組む前は、Prism およびその他のクライアント ガイダンスを担当する patterns & practices の製品プランナでした。Glenn は卓越した知識を持ち、ALT.NET などのカンファレンスやグループでその専門知識を広めることに時間を費やしています。

Page view tracker