July 2009
Volume 24 Number 07
Silverlight - Prism を使用した複合 Web アプリケーション
Shawn Wildermuth | July 2009
コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコード参照
ここでは、以下について説明します。
|
この記事では、次のテクノロジを使用しています。 Silverlight 2、Prism |
目次
依存関係の注入を導入する
起動の動作
モジュール化
UI コンポジション
イベント アグリゲーション
デリゲート コマンド
まとめ
皆さんの Silverlight との出会いは、おそらく非常に小さなアプリケーション、たとえば、ビデオ プレーヤーやシンプルなグラフ作成アプリケーション、もしかしたら単なるメニューだったかもしれません。これらは単純で、簡単に設計できます。このようなタイプの設計で、個別の役割を持つ厳密な層へのセグメント化を用いるのは過剰な手法でしょう。
しかし、密結合スタイルを大規模なアプリケーションに適用しようとすると問題が表面化します。さまざまな移動パーツが増えて、アプリケーション開発の単純なスタイルは崩れます。対策の一部として階層化を使用します (私の記事「Silverlight 2 アプリケーションでの Model-View-ViewModel」を参照)。このような大規模な Silverlight プロジェクトにおいて、密結合アーキテクチャは、解決が必要なさまざまな問題の 1 つにすぎません。
この記事では、Prism プロジェクトによる Composite Application Library の複合技術を使用してアプリケーションを構築する方法を示します。私のサンプルは、データベース データの単純なエディタです。
要件の変化とプロジェクトの成熟に応じて、システム全体に影響を与えることなくアプリケーションの各パーツを変更できると便利です。アプリケーションをモジュール化すると、アプリケーション コンポーネントを別々に (疎結合で) 構築することができ、残りのコードに影響を与えずにアプリケーションのパーツ全体を変更できます。
また、アプリケーションのすべてのパーツを一度に読み込む必要がない場合もあります。たとえば、ある顧客管理アプリケーションで、ユーザーがログオンして見込み客のパイプラインを管理したり、任意の見込み客からの電子メールをチェックしたりできるとします。ユーザーが電子メールを日に何度もチェックし、パイプラインを毎日または 1 日おきにだけ管理する場合、パイプラインを管理するコードは必要に応じて読み込めばよいでしょう。このケースでは、部分的なオンデマンドの読み込みをアプリケーションでサポートするのが理想的です。この問題は、アプリケーションのモジュール化で解決できます。
Microsoft の patterns & practices チームは、Prism (または CompositeWPF) プロジェクトを作成しました。その目的は、Windows Presentation Foundation (WPF) アプリケーションで発生する上記のような問題に対処することです。なお、Prism は、Silverlight をサポートできるように更新されています。Prism パッケージには、アプリケーション構築のためのフレームワークとガイダンスが共に含まれています。CAL (Composite Application Library) と呼ばれるフレームワークでは、以下の処理が可能です。
- アプリケーションのモジュール化: パーティション化されたコンポーネントでアプリケーションを構築します。
- UI コンポジション: アプリケーションの残りの部分の個別の知識なしで、疎結合コンポーネントによりユーザー インターフェイスを作成できるようにします。
- サービス ロケーション: 水平方向のサービス (ログインや認証など) と垂直方向のサービス (ビジネス ロジック) とを分けて、アプリケーションの明確な階層化を促進します。
CAL は、この同一の設計原則に基づいて記述されています。アプリケーション開発者にとって、CAL はまさにビュッフェ スタイルのフレームワークです。必要なものだけを自分の皿に取り、そうでないものには手を付けないのですから。図 1 は、皆さんの固有のアプリケーションとの関係における CAL の基本的なレイアウトを示しています。
図 1 Composite Application Library
CAL は、これらのサービスをサポートして、開発者がアプリケーションを小さなパーツから作成できるようにします。つまり、CAL は基本的機能を提供するだけでなく、(いつ) どのパーツを読み込むかについても支援を提供します。開発者は、これらの機能のどれが自分のジョブに役立ち、どれが不要であるかを決定できます。
この記事で紹介する私のサンプルは、できるだけ多く CAL を使用しています。これはシェル アプリケーションであり、CAL を使用して実行時に複数のモジュールを読み込み、ビューを領域に配置し (図 2 を参照)、サービスをサポートします。ただし、そのコードに取りかかる前に、依存関係の注入 (IoC (Inversion of Control) とも呼ばれる) の基本的な概念を理解する必要があります。CAL の機能の多くは依存関係の注入に依存しているため、その基本を理解することは、Prism を使用した Silverlight プロジェクトのアーキテクチャの開発に役立ちます。
図 2 複合アプリケーションのアーキテクチャ
依存関係の注入を導入する
一般的な開発では、プロジェクトはエントリ ポイント (実行可能な default.aspx ページなど) で開始されます。アプリケーションを 1 つの大規模なプロジェクトとして開発する場合もありますが、ほとんどのケースではある程度のモジュール化が使用されており、アプリケーションはプロジェクトのパーツである複数のアセンブリを読み込みます。メイン アセンブリは、必要なアセンブリがどれであるかを認識しており、その固定参照を作成します。コンパイル時に、メイン プロジェクトは参照先アセンブリをすべて認識しており、ユーザー インターフェイスは静的コントロールで構成されます。アプリケーションはどれが必要なコードであるかについて管理し、通常は、使用する可能性のあるすべてのコードを認識します。ただし、これこそが問題なのです。なぜなら、開発はメイン アプリケーション プロジェクト内から開始されるからです。モノリシック アプリケーションが大きくなるにつれて、ビルド時間と競合の変化により開発が遅れることがあります。
依存関係の注入の目的は、実行時に依存関係を設定する命令を提供することで、この状況を一変させることです。依存関係の注入は、依存関係を制御するプロジェクトではなく、コンテナと呼ばれるコードで行います。
なぜ、これが重要なのでしょうか。1 つには、コードのモジュール化によりテストが簡単になるからです。プロジェクトの依存関係を交換できるので、より明確なテストが可能です。テストのエラーの原因を特定するときにテスト対象のコードだけを調べることができます。依存関係の入れ子になったチェーンのどこかにあるコードを調べる必要はありません。具体的な例をご紹介します。たとえば、あなたのコンポーネントが自分だけでなく、他の開発者も特定の企業の住所検索に利用できるとします。そのコンポーネントは、データを自動的に入手するデータ アクセス コンポーネントに依存しています。コンポーネントをテストするときに、データベースに対するテストから開始したところ、いくつかのテストでエラーが出ました。しかし、データベースのスキーマおよびビルドが常に変化しているため、テストのエラーの原因が自分の記述したコードだったのか、またはデータ アクセス コードだったのかがわかりません。このケースのコンポーネントはデータ アクセス コンポーネントに対して強い依存関係を持っているため、アプリケーションのテストの信頼性が低くなり、自分のコードや別の開発者のコードでエラーを追跡するときにチャーンが発生します。
このコンポーネントは、次に示すようなものでしょう。
public class AddressComponent
{
DataAccessComponent data = new DataAccessComponent();
public AddressComponent()
{
}
...
}
次に示すように、組み込みのコンポーネントの代わりに、データ アクセスを表すインターフェイスを受け取ることができます。
public interface IDataAccess
{
...
}
public class AddressComponent
{
IDataAccess data;
public AddressComponent(IDataAccess da)
{
data = da;
}
...
}
通常、インターフェイスを使用して、コードを調整するためのバージョンを作成できます。この手法はしばしば "モッキング" と呼ばれます。モッキングとは、実際には真のバージョンを表さない依存関係の実装を意味します。まさに、見せかけの実装を作成しているのです。
この手法の方が好ましい理由は、オブジェクトの構築中に依存関係 (IDataAccess) をプロジェクトに注入できることです。IDataAccess コンポーネントの実装は、要件 (テストまたは本番) によって異なります。
本質的には、依存関係の注入がどのように動作するかに基づいています。では、注入はどのように処理されるでしょうか。型の作成を処理することがコンテナのジョブであり、コンテナを使用することによって型を登録し、解決することができます。たとえば、あなたが IDataAccess インターフェイスを実装する具象クラスを持っているとします。アプリケーションの起動中に、その型を登録するようにコンテナに指示できます。次に示すように、アプリケーションのその他の任意の場所 (あなたが型を必要とする場所) で型が解決されるようにコンテナに要求できます。
public void App_Startup()
{
container.RegisterType<IDataAccess, DbDataAccess>();
}
...
public void GetData()
{
IDataAccess acc = container.Resolve<IDataAccess>();
}
状況に応じて (テストまたは本番)、登録を変更するだけで IDataAccess の実装を切り替えることができます。さらに、コンテナは依存関係の構築の注入を処理できます。コンテナのコンストラクタによって作成する必要のあるオブジェクトが、コンテナで解決可能なインターフェイスを受け取った場合、型を解決してコンストラクタに渡します (図 3 を参照)。
図 3 コンテナによる型解決
public class AddressComponent : IAddressComponent
{
IDataAccess data;
public AddressComponent(IDataAccess da)
{
data = da;
}
}
...
public void App_Startup()
{
container.RegisterType<IAddressComponent, AddressComponent>();
container.RegisterType<IDataAccess, DbDataAccess>();
}
public void GetAddresses()
{
// When we ask the container to create the AddressComponent,
// it sees that a constructor takes a IDataAccess object
// so it automatically resolves that dependency
IAddressComponent addr = container.Resolve<IAddressComponent>();
}
AddressComponent のコンストラクタが IDataAccess の実装を受け取ることに注意してください。コンストラクタは解決時に AddressComponent クラスを作成する場合、IDataAccess のインスタンスを自動的に作成し、それを AddressComponent に渡します。
型をコンテナに登録するときに、型の有効期間を特別な方法で処理するようにコンテナに指示します。たとえば、ログ コンポーネントを使用している場合、シングルトンとして扱うことにより、ログを必要とするアプリケーションの各パーツが自身のコピーの取得を行わないようにすることができます (既定の動作)。そのために、抽象クラス LifetimeManager の実装を供給できます。いくつかの有効期間マネージャがサポートされます。ContainerControlledLifetimeManager は、プロセスごとのシングルトンです。PerThreadLifetimeManager は、スレッドごとのシングルトンです。ExternallyControlledLifetimeManager の場合、コンテナはシングルトンへの弱参照を保持します。オブジェクトが外部で解放されたときに、コンテナは新しいインスタンスを作成します。そうでない場合は、弱参照に含まれているライブ オブジェクトを返します。
LifetimeManager クラスを使用するには、型を登録するときに指定します。その例を次に示します。
container.RegisterType<IAddressComponent, AddressComponent>( new ContainerControlledLifetimeManager());
CAL では、IoC コンテナは patterns & practices グループの Unity フレームワークに基づいています。次の例で Unity コンテナを使用していますが、Unity IoC コンテナの代わりに使用されるオープン ソースもあります (Ninject、Spring.NET、Castle、StructureMap など)。IoC コンテナを使い慣れていて、既に Unity の代わりに使用している場合は、固有のコンテナを提供できます (ただし、こちらは少し追加の手順が必要です)。
起動の動作
通常、Silverlight アプリケーションの起動の動作は、単純にメイン XAML ページのクラスを作成して、アプリケーションの RootVisual プロパティに割り当てることです。この操作は複合アプリケーションでも必要ですが、通常、複合アプリケーションでは XAML ページ クラスを作成する代わりに、ブートストラップ クラスを使用して起動の動作を処理します。
起動するには、UnityBootstrapper クラスから派生した新しいクラスが必要です。このクラスは、Microsoft.Practices.Composite.UnityExtensions アセンブリ内にあります。ブートストラップには、起動の動作のさまざまなパーツを処理する、オーバーライド可能なメソッドが含まれています。すべての起動メソッドではなく、必要なものだけオーバーライドします。オーバーライドが必要な 2 つのメソッドは、CreateShell と GetModuleCatalog です。
CreateShell メソッドは、メイン XAML クラスが作成される場所です。これはアプリケーションのコンポーネントのビジュアル コンテナなので、通常、シェルと呼ばれます。私の例では、次に示すようにブートストラップを使用して、Shell クラスの新しいインスタンスを作成して RootVisual に割り当て、その後に新しい Shell クラスを返します。
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
Shell theShell = new Shell();
App.Current.RootVisual = theShell;
return theShell;
}
protected override IModuleCatalog GetModuleCatalog()
{
...
}
}
GetModuleCatalog メソッドについては次のセクションで説明しますが、このメソッドは読み込むモジュールのリストを返します。
これでブートストラップ クラスを入手できました。このクラスを Silverlight アプリケーションの起動メソッドに使用できます。通常は、ブートストラップ クラスの新しいインスタンスを作成して、その Run メソッドを呼び出します (図 4 を参照)。
図 4 ブートストラップのインスタンスを作成する
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
this.Exit += this.Application_Exit;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
Bootstrapper boot = new Bootstrapper();
boot.Run();
}
...
}
ブートストラップは、アプリケーションのさまざまなパーツで必要とされる、コンテナへの型の登録にも関連しています。これを達成するには、ブートストラップの ConfigureContainer メソッドをオーバーライドします。これにより、アプリケーションの残りの部分で使用される任意の型を登録できるようになります。図 5 はそのコードを示しています。
図 5 型を登録する
public class Bootstrapper : UnityBootstrapper
{
protected override void ConfigureContainer()
{
Container.RegisterType<IShellProvider, Shell>();
base.ConfigureContainer();
}
protected override DependencyObject CreateShell()
{
// Get the provider for the shell
IShellProvider shellProvider = Container.Resolve<IShellProvider>();
// Tell the provider to create the shell
UIElement theShell = shellProvider.CreateShell();
// Assign the shell to the root visual of our App
App.Current.RootVisual = theShell;
// Return the Shell
return theShell;
}
protected override IModuleCatalog GetModuleCatalog()
{
...
}
}
このコードでは、私の例で作成した、(CAL フレームワークの一部ではない) IShellProvider インターフェイスを実装するクラスのインターフェイスを登録します。このようにして、CreateShell メソッドの実装で使用できるようにします。インターフェイスを解決し、それを使用してシェルのインスタンスを作成することにより、インスタンスを RootVisual に割り当てて返すことができます。この方法は余分な作業のように見えますが、CAL でアプリケーションを構築する方法を掘り下げていくと、このブートストラップがどのように役立つのかについてはっきりわかってきます。
モジュール化
標準的な .NET 環境で、アセンブリは主要な作業単位です。この指定により、開発者は互いに他のコードと区別して自分のコードで作業できます。CAL では、これらの作業単位がそれぞれモジュールであり、CAL でモジュールを使用するには、モジュールの起動の動作と通信できるクラスが必要です。このクラスで IModule インターフェイスをサポートする必要もあります。IModule インターフェイスには、Initialize という 1 つのメソッドが必要です。このメソッドを使用すると、モジュールがアプリケーションの残りの部分で使用されるようにモジュール自体が設定します。この例には、アプリケーションのログ機能を含む ServerLogger モジュールが含まれています。次に示すように、ServerLoggingModule クラスは IModule インターフェイスをサポートします。
public class ServerLoggerModule : IModule
{
public void Initialize()
{
...
}
}
問題は、モジュールで何を初期化するのかがわからないことです。ServerLogging モジュールなので、ログを自動的に行う型を登録するのが論理的に思えます。コンテナを使用して型を登録することにより、実行する正確なログの型がわからなくても、ログ機能を必要とするだれもが単純に私たちの実装を使用できるようにしたいのです。
コンテナを入手するには、IUnityContainer インターフェイスを受け取るコンストラクタを作成します。依存関係の注入に関する説明を思い出してください。コンテナは、コンストラクタの注入を使用して、既知の型を追加します。IUnityContainer はアプリケーション内のコンテナを表すので、このコンストラクタを追加すると、保存して初期化などで使用することができます。
public class ServerLoggerModule : IModule
{
IUnityContainer theContainer;
public ServerLoggerModule(IUnityContainer container)
{
theContainer = container;
}
public void Initialize()
{
theContainer.RegisterType<ILoggerFacade, ServerBasedLogger>(
new ContainerControlledLifetimeManager());
}
}
初期化が完了したら、このモジュールはアプリケーションのログ実装の役割を担います。ところで、このモジュールはどのようにして読み込まれるのでしょうか。
CAL を使用してアプリケーションを作成するときに、アプリケーションのモジュールをすべて含む ModuleCatalog を作成する必要があります。このカタログを作成するには、ブートストラップの GetModuleCatalog 呼び出しをオーバーライドします。Silverlight では、このカタログをコードまたは XAML によって設定できます。
コードを使用して、ModuleCatalog クラスの新しいインスタンスを作成し、モジュールで設定します。たとえば、次のようになります。
protected override IModuleCatalog GetModuleCatalog()
{
var logModule = new ModuleInfo()
{
ModuleName = "ServerLogger",
ModuleType = "ServerLogger.ServerLoggerModule, ServerLogger, Version = 1.0.0.0"
};
var catalog = new ModuleCatalog();
catalog.AddModule(logModule);
return catalog;
}
ここで、私は単純に ServerLogger というモジュールを 1 つ追加しています。これは、ModuleInfo の ModuleType プロパティで型定義されています。さらに、モジュール間の依存関係を指定できます。一部のモジュールが他のモジュールに依存する場合があるので、依存関係を指定することにより、カタログは依存関係を提供する順序を認識できます。ModuleInfo.DependsOn プロパティを使用して、どの名前付きモジュールが別のモジュールを読み込む必要があるかを指定できます。
次に示すように、カタログを XAML ファイルから直接読み込むことができます。
protected override IModuleCatalog GetModuleCatalog()
{
var catalog = ModuleCatalog.CreateFromXaml(new Uri("catalog.xaml",
UriKind.Relative));
return catalog;
}
XAML ファイルには、コードで作成できるものと同じ型情報が含まれています。XAML を使用する利点は、実行中に変更できることです (どのユーザーがログオンしているかに基づき、XAML ファイルをサーバーや他の場所から取得することを想定してみましょう)。図 6 に、catalog.xaml ファイルの例を示します。
図 6 サンプル Catalog.xaml ファイル
<m:ModuleCatalog
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:m="clr-namespace:Microsoft.Practices.Composite.Modularity;
assembly=Microsoft.Practices.Composite">
<m:ModuleInfoGroup InitializationMode="WhenAvailable">
<m:ModuleInfo ModuleName="GameEditor.Client.Data"
ModuleType="GameEditor.Client.Data.GameEditorDataModule,
GameEditor.Client.Data, Version=1.0.0.0"/>
<m:ModuleInfo ModuleName="GameEditor.GameList"
ModuleType="GameEditor.GameList.GameListModule,
GameEditor.GameList, Version=1.0.0.0"
InitializationMode="WhenAvailable">
<m:ModuleInfo.DependsOn>
<sys:String>GameEditor.Client.Data</sys:String>
</m:ModuleInfo.DependsOn>
</m:ModuleInfo>
</m:ModuleInfoGroup>
</m:ModuleCatalog>
この XAML カタログでは、グループに 2 つのモジュールが含まれ、2 番目のモジュールは最初のモジュールに依存しています。コードの場合と同じように、ロールまたはアクセス許可に基づいて特定の XAML カタログを使用できます。
ブートストラップにより読み込まれたカタログは、モジュール クラスのインスタンスの作成を試行し、インスタンス自身で初期化が行われるようにしようとします。ここに示すコード例では、カタログが動作できるようにするために、型をアプリケーションで参照する必要があります (したがって、既にメモリに読み込まれています)。
Silverlight にとってこの機能が必要不可欠になるのが、この場面です。作業単位はアセンブリですが、1 つまたは複数のモジュールを含む .xap ファイルを指定できます。そのためには、ModuleInfo の Ref 値を指定します。Ref 値は、モジュールを含む .xap ファイルへのパスです。
protected override IModuleCatalog GetModuleCatalog()
{
var logModule = new ModuleInfo()
{
ModuleName = "ServerLogger",
ModuleType =
"ServerLogger.ServerLoggerModule, ServerLogger, Version= 1.0.0.0",
Ref = "ServerLogger.xap"
};
var catalog = new ModuleCatalog();
catalog.AddModule(logModule);
return catalog;
}
.xap ファイルを指定すると、ブートストラップはアセンブリを利用できないことを認識し、サーバーにアクセスして .xap ファイルを非同期に取得します。.xap ファイルが読み込まれた後、Prism はアセンブリを読み込み、モジュール型を作成し、モジュールを初期化します。
複数のモジュールを含む .xap ファイルの場合、ModuleInfo オブジェクトのセットを含む ModuleGroup を作成し、1 つの .xap ファイルからすべてのモジュールが読み込まれるように ModuleGroup の Ref を設定することができます。
var modGroup = new ModuleInfoGroup();
modGroup.Ref = "MyMods.xap";
modGroup.Add(logModule);
modGroup.Add(dataModule);
modGroup.Add(viewModule);
var catalog = new ModuleCatalog();
catalog.AddGroup(modGroup);
Silverlight アプリケーションの場合、これは複数の .xap ファイルからアプリケーションを作成する方法であり、作成したアプリケーションの異なるセクションのバージョンは個別に管理できます。
.xap ファイルに含める Silverlight モジュールを作成する場合、(Silverlight ライブラリではなく) Silverlight アプリケーションを作成することになります。そして、.xap ファイルに格納するモジュール プロジェクトをすべて参照します。app.xaml ファイルおよび page.xaml ファイルを削除する必要があります。なぜなら、この .xap ファイルは通常の .xap ファイルとは異なり、読み込まれて実行されることはないからです。この .xap ファイルは、単なるコンテナです (.zip ファイルの場合もありますが、それは問題ではありません)。また、メイン プロジェクトで既に参照されているプロジェクトを参照する場合は、その参照をプロパティで Copy Local=false に変更できます。.xap ファイルにアセンブリは必要ないからです (メイン アプリケーションは既にアセンブリを読み込み済みなので、カタログは再度それを読み込もうとはしません)。
ただし、ネットワーク経由での複数の呼び出しを含む大規模なアプリケーションを読み込む場合は、これでパフォーマンスが向上するとは思えません。その場合は、ModuleInfo の InitializationMode プロパティが役に立ちます。InitializationMode では、WhenAvailable と OnDemand という 2 つのモードがサポートされます。WhenAvailable では、.xap ファイルが非同期に読み込まれて、初期化されます (これは既定の動作です)。OnDemand では、.xap は明示的に要求された場合に読み込まれます。モジュール カタログでは、初期化の前にはモジュール内の型が認識されていないので、OnDemand で初期化される型を解決しようとすると失敗します。
モジュールおよびグループに対してオンデマンドがサポートされるので、必要に応じて、大規模なアプリケーションの特定の機能を読み込むことができます。起動時間が短縮され、他の必要なコードはユーザーがアプリケーションとやり取りしながら読み込むことができます。これは、アプリケーションの個別のパーツに対する承認を持っている場合に適したすばらしい機能です。アプリケーションのいくつかのパーツのみを必要とするときには、まったく使用しないコードはダウンロードしなくて済みます。
モジュールをオンデマンドで読み込むには、IModuleManager インターフェイスにアクセスする必要があります。ほとんどの場合は、オンデマンドでモジュールを読み込む必要のあるクラスのコンストラクタで、このアクセスを要求した後、LoadModule を呼び出すことにより、IModuleManager を使用してモジュールを読み込みます (図 7 を参照)。
図 7 LoadModule を呼び出す
public class GameListViewModel : IGameListViewModel
{
IModuleManager theModuleManager = null;
public GameListViewModel(IModuleManager modMgr)
{
theModuleManager = modMgr;
}
void theModel_LoadGamesComplete(object sender,
LoadEntityCompleteEventArgs<Game> e)
{
...
// Since we now have games, let's load the detail pane
theModuleManager.LoadModule("GameEditor.GameDetails");
}
}
モジュールは、単純にアプリケーション内のモジュール化の単位です。Silverlight でのモジュールの扱いは、ライブラリ プロジェクトの場合とよく似ています。ただし、モジュールの初期化という作業が追加されており、これによってモジュールをメイン プロジェクトから切り離すことができます。
UI コンポジション
一般的なエクスプローラ アプリケーションでは、左側のウィンドウに情報のツリーの一覧が表示され、そこで選択したアイテムの詳細が右側のウィンドウに表示されます。CAL では、これらのエリアが領域と呼ばれます。
CAL は、RegionManager クラスで添付プロパティを使用することにより、XAML で領域を直接定義できます。このプロパティを使用して、シェル内の領域を指定し、その領域でどのビューをホストするかを示すことができます。たとえば、次に示すように、LookupRegion および DetailRegion という 2 つの領域がシェルにあるとします。
<UserControl
...
xmlns:rg=
"clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;
assembly=Microsoft.Practices.Composite.Presentation">
...
<ScrollViewer rg:RegionManager.RegionName="LookupRegion" />
<ScrollViewer rg:RegionManager.RegionName="DetailRegion" />
</UserControl>
RegionName は、ItemsControl とその派生コントロール (ListBox など)、Selector とその派生コントロール (TabControl など)、および ContentControl とその派生コントロール (ScrollViewer など) に適用できます。
次に示すように、領域を定義した後、IRegionManager インターフェイスを使用して、ビューが領域に読み込まれるようにモジュールに指示できます。
public class GameListModule : IModule
{
IRegionManager regionManager = null;
public GameListModule(IRegionManager mgr)
{
regionManager = mgr;
}
public void Initialize()
{
// Build the View
var view = new GameListView();
// Show it in the region
regionManager.AddToRegion("LookupRegion", view);
}
}
この機能により、ビューを表示できるアプリケーションの領域を定義し、ビューを領域に配置する方法を定義するようにモジュールに指示して、シェルが完全にビューを認識しなくても済むように設定できます。
領域の動作は、ホストされるコントロールの型によって異なる場合があります。この例では ScrollViewer を使用しているので、1 つのビューだけを領域に追加できます。これに対して、ItemControl 領域では、複数のビューを追加できます。各ビューを追加するたびに、ItemsControl の新しいアイテムとして表示されます。この機能により、ダッシュボードなどの構築が簡単になります。
MVVM パターンを使用してビューを定義する場合は、コンテナのサービス ロケーション要素と領域とを混在させることで、ビューおよびビュー モデルが互いに認識されないようにして、実行時にモジュールを結合することができます。たとえば、GameListModule を変更する場合、ビューおよびビュー モデルをコンテナに登録し、その後で結合してから、ビューを領域に適用することができます (図 8 を参照)。
図 8 領域のビューを結合する
public class GameListModule : IModule
{
IRegionManager regionManager = null;
IUnityContainer container = null;
public GameListModule(IUnityContainer con, IRegionManager mgr)
{
regionManager = mgr;
container = con;
}
public void Initialize()
{
RegisterServices();
// Build the View
var view = container.Resolve<IGameListView>();
// Get an Implemenation of IViewModel
var viewModel = container.Resolve<IGameListViewModel>();
// Marry Them
view.ApplyModel(viewModel);
// Show it in the region
regionManager.AddToRegion("LookupRegion", view);
}
void RegisterServices()
{
container.RegisterType<IGameListView, GameListView>();
container.RegisterType<IGameListViewModel, GameListViewModel>();
}
}
この方法では、厳密な MVVM の分離を維持しつつ、UI コンポジションを使用することができます。
イベント アグリゲーション
UI コンポジションを通じてアプリケーション内に複数のビューアを保持した場合に、開発者が直面する一般的な問題があります。独立したビューを構築して、より優れたテストおよび開発をサポートした場合であっても、しばしば、ビューを完全には分離できない部分があります。これらは通信する必要があるので論理的には結合されていますが、論理的な結合の有無にかかわらず、可能な限り緩やかに結合された状態にするのが望ましい場合があります。
疎結合およびビュー間の通信を可能にするために、CAL ではイベント アグリゲーションというサービスをサポートしています。イベント アグリゲーションでは、グローバル イベントのパブリッシャおよびコンシューマがコードのさまざまなパーツにアクセスできるようにすることが可能です。このようなアクセスにより、密結合なしで簡単に通信できる方法が提供されます。これは、CAL の IEventAggregator インターフェイスで実現可能です。IEventAggregator を使用すると、アプリケーションの異なるモジュールに対してイベントの発行と購読を行うことができます。
通信を可能にするには、EventBase の派生クラスが必要です。通常は、CompositePresentationEvent<T> クラスから派生させた簡単なイベントを作成します。このジェネリック クラスにより、発行するイベントのペイロードを指定できます。この場合は、ユーザーがゲームを選択すると GameListViewModel でイベントが発行されるので、ユーザーによるゲームの選択時にコンテキストを変更しようとする他のコントロールは、そのイベントを購読できます。私たちのイベント クラスは、次のようになります。
public class GameSelectedEvent : CompositePresentationEvent<Game>
{
}
イベントが定義済みになると、イベント アグリゲータは GetEvent メソッドを呼び出すことによりイベントを発行できます。これにより、アグリゲートするシングルトン イベントが取得されます。このメソッドを最初に呼び出したアグリゲータにより、シングルトンが作成されます。そのイベントから Publish メソッドを呼び出してイベントを作成できます。イベントを発行することは、イベントを発生させることに似ています。イベントで情報を送信する必要が生じるまでは、イベントの発行は必要はありません。たとえば、この例では、GameList でゲームが選択されると、新しいイベントを使用してそのゲームを発行します。
// Fire Selection Changed with Global Event
theEventAggregator.GetEvent<GameSelectedEvent>().Publish(o as Game);
作成したアプリケーションの他のパーツでは、イベントの発行後に呼び出されるイベントを購読できます。イベントの Subscribe メソッドにより、イベントが発行されたときに呼び出すメソッドを指定したり、イベントを呼び出すためのスレッド セマンティクス (たとえば、UI スレッドがよく使用される) を要求するためのオプションを指定したりできます。また、渡された情報への参照をアグリゲータで保持してガベージ コレクションの影響を回避するかどうかを、指定することができます。
// Register for the aggregated event
aggregator.GetEvent<GameSelectedEvent>().Subscribe(SetGame,
ThreadOption.UIThread,
false);
サブスクライバとしては、特定の状況でのみ呼び出すフィルタを指定することもできます。たとえば、アプリケーションの状態を返すイベントと、データが特定の状況にある間のみ呼び出されるフィルタがあるとします。
イベント アグリゲータにより、密結合なしでモジュール間の通信を実現できます。決して発行されないイベントを購読した場合、または決して購読されないイベントを発行した場合、コードのエラーは絶対に発生しません。
デリゲート コマンド
Silverlight では (WPF とは異なり)、真のコマンド インフラストラクチャは存在しません。そのため、XAML でコマンド インフラストラクチャを使用して直接的に簡単に達成されるタスクでも、ビューの分離コードを使用することが強制されます。Silverlight がこの機能をサポートするまでは、この問題を解決するためのクラスを CAL でサポートします。それが DelegateCommand です。
DelegateCommand の使用を開始するには、ViewModel で DelegateCommand をデータバインドできるように定義する必要があります。ViewModel で新しい DelegateCommand を作成します。DelegateCommand は、受け取るデータの型 (データが使用されない場合、たいていは Object)、および 1 つまたは 2 つのコールバック メソッド (またはラムダ関数) を予測します。これらのメソッドの最初の 1 つは、コマンドが起動されるときに実行するアクションです。必要に応じて、コマンドを起動できるかどうかをテストするために呼び出される 2 つ目のコールバックを指定できます。コマンドの起動が有効でないときに、UI (ボタンなど) 内のオブジェクトを無効化できるようにするという考え方です。たとえば、私たちの GameDetailsViewModel にはデータの保存をサポートするコマンドが含まれています。
// Create the DelegateCommand
SaveCommand = new DelegateCommand<object>(c => Save(), c => CanSave());
SaveCommand が実行されるときに、ViewModel で Save メソッドが呼び出されます。次に、CanSave メソッドが呼び出されて、コマンドが有効であることを確認します。これにより、DelegateCommand は必要に応じて UI を無効化できます。ビューの状態が変わった場合、DelegateCommand.RaiseCanExecuteChanged メソッドを呼び出し、CanSave メソッドの新しい検査を強制して、必要に応じて UI を有効化または無効化することができます。
これを XAML にバインドするには、Microsoft.Practices.Composite.Presentation.Commands 名前空間の Click.Command 添付プロパティを使用します。自分の ViewModel などで使用するコマンドになるように、コマンドの値をバインドします。
<Button Content="Save"
cmd:Click.Command="{Binding SaveCommand}"
Style="{StaticResource ourButton}"
Grid.Column="1" />
これで、Click イベントが発生したときに、コマンドが実行されます。必要に応じて、コマンド パラメータがコマンドに送信されるように指定して、再利用することができます。
お気付きかもしれませんが、CAL に存在する唯一のコマンドが、ボタンの Click イベント (またはその他のセレクタ) です。ただし、固有のコマンドを記述するときに使用できるクラスは、非常に簡単です。サンプル コードには、ListBox/ComboBox の SelectionChanged のコマンドが含まれています。このコマンドは、SelectorCommandBehavior という名前で、CommandBehaviorBase<T> クラスから派生しています。カスタム コマンドの動作の実装を確認することにより、独自のコマンドの動作を記述するときの開始点がわかります。
まとめ
大規模な Silverlight アプリケーションの開発には、明確な問題点があります。疎結合およびモジュール化を使用してアプリケーションを構築することにより、その利点を活用し、変化に即応することができます。マイクロソフトの Prism プロジェクトで提供されるツールおよびガイダンスを使用して、開発者は自分のプロジェクトで敏捷性を向上させることができます。Prism は画一的なアプローチではありませんが、CAL のモジュール化の意味は、開発者がそれぞれ固有のシナリオに適したものだけを使用し、それ以外は入手せずに済むということです。
Shawn Wildemuth は Microsoft MVP (C#) であり、Wildermuth Consulting Services の創立者でもあります。彼は数冊の著書と多数の記事を執筆しています。また、現在は各地で Silverlight 2 を教える Silverlight ツアーを実施しています。連絡先は shawn@wildermuthconsulting.com です。