マルチプラットフォーム開発
ポータブル クラス ライブラリ: 入門
Bill Kratochvil
ポータブル クラス ライブラリ (PCL: Portable Class Library) プロジェクトは、Windows Phone 7、Silverlight、Microsoft .NET Framework、および Xbox 360 の各プラットフォームで参照できるマネージ アセンブリを生成します。これにより、コードを最大限に再利用し、必要なプロジェクトの数を削減できます。特に、この記事付属のデモ アプリケーションのように、同じコードベースを共有するマルチターゲット アプリケーションの場合にこの効果が顕著に現れます。この記事では、Windows Phone 7 アプリケーションの作成に時間をかけました。その結果、WPF アプリケーションや Silverlight アプリケーションにも無理なく移植できました。唯一の制約は、PCL からプラットフォーム固有のプロジェクトを参照できないことです。参照できるのは、他の PCL プロジェクトだけです。一見、これは制限事項のように思えます。Prism や依存関係の挿入 (DI: Dependency Injection) を利用するアプリケーションの場合はなおさらです。しかし、注意深く計画すればこの制約を回避でき、優れた開発プラクティスの実践に役立つ効率的な PCL プロジェクトを作成できます。
PCL プロジェクトより前のソリューション プロジェクトは、同じプラットフォームのアセンブリしか参照できませんでした。つまり、Silverlight プロジェクトなら他の Silverlight アセンブリを、.NET プロジェクトなら他の .NET アセンブリをといった参照しかできませんでした。効果的にコーディングするため、管理できないほどプロジェクトの数が増えていきます。共有コードベース (すべてのプラットフォームで使用可能なコード) を作成するときは、プラットフォームごとにプロジェクトを作成する必要があるためです。
マルチターゲットの Password Manager ソリューション
Password Manager ソリューション (passwordmgr.codeplex.com、英語) は、1 つのコードベースを使用するマルチターゲット アプリケーションです (コードベースは、Windows Phone 7 プロジェクト、および Windows Phone 7 プロジェクトのファイルにリンクする Silverlight プロジェクトと WPF プロジェクトによってホストされます)。ご存知のように、この小さなアプリケーションには、気が遠くなるほどの数のプロジェクトが存在します。
なぜ、そんなに多くのプロジェクトが存在するのでしょうか。これは、クライアント ソリューション向けにフレームワークを設計していたとき、開発者からよく耳にした質問です。「プロジェクトが多いと、ソリューションが複雑になり、理解しにくくなる」と繰り返し言われました。良い意見です。
このような質問にはいつも、「懸案事項を明確に分離し、最大限に再利用するため」と答えていました。プロジェクトにはそれぞれ 1 つの目的があります (そして、それぞれを疎結合方式でうまく機能させます)。その適例として、Password Manager ソリューションで利用可能な明確な分離と、DI を使用して異なるデータ アクセス層 (DAL: Data Access Layer) を簡単に置き換える機能があります。ただし、SQLite を使用するつもりがなくても (DAL に SQL Server を使用するつもりであっても)、このコードを再利用するには、SQLite アセンブリと関連プロジェクトを取得する必要があります。
いったん他のプロジェクトを参照するとプロジェクトは密結合となり、そのプロジェクトだけでなく、そのプロジェクトのすべての依存関係も、他のプロジェクトに持ち込むことになります。少数のプロジェクトしか使用していなくても、コードを他のソリューションで再利用するのがますます難しくなります。
PCL は管理すべきプロジェクトの数を大幅に削減します。これは特に、他のモジュールやソリューションからプロジェクトを簡単に再利用するために、懸案事項を明確に分離したい場合に顕著です。重要なのは、インターフェイスをプログラミングすることによって、プロジェクトを疎結合に保つことです。これにより、インターフェイスの実装を簡単に構成できるようにする Managed Extensibility Framework (MEF)、Unity などの DI フレームワークの使用が可能になります。つまり、DAL のインターフェイスの実装を、SQL Server、クラウド、または SQLite のクラスにすることができます。
前提条件をインストールしたら (bit.ly/fxatk0、英語)、[新しいプロジェクトの追加] ダイアログ ボックスの PCL を作成する新機能が使用できるようになります。
DI フレームワークの使用
これらの強力な DI フレームワークの使用を試みるとき、「Dependency 属性や Export 属性といった DI フレームワークのコンポーネントを参照できないとしたら、どのようにしてこれらのフレームワークを使って PCL を利用するのか」という疑問がすぐに頭に浮かぶことでしょう。たとえば、次のコードの SecurityViewModel には Unity の Dependency 属性があり、この属性が ISecurityViewModel の実装を解決します。
namespace MsdnDemo.MvpVmViewModels
{
public class SecurityViewModel : PresentationViewModelBase
{
[Dependency]
public ISecurityViewModel UserInfo { get; set; }
public bool IsAuthenticated
{
get { return UserInfo.IsAuthenticated; }
set
{
UserInfo.IsAuthenticated = value;
OnPropertyChanged("IsAuthenticated");
if(value)
{
IsAdmin = UserInfo.IsInRole("Admin");
IsGuest = UserInfo.IsInRole("Guest");
}
}
}
}
}
参照できるのは他の PCL プロジェクトだけなので、DI や再利用可能なライブラリに含まれる他の共有リソースと PCL を併用することは実用的ではないでしょう。現実には、この制約は実際に懸案事項を明確に分離するのに役立ち、プロジェクトをより再利用可能にさえします。つまり、将来は DI を利用せず、PCL プロジェクトのメリットを活かすプロジェクトになっていく可能性があります。
DI と PCL を併用する原理を、デモ アプリケーションを使用して示します。ロールベースのセキュリティを使用して、オンデマンドで読み込む 2 つのモジュール (ゲストとメイン) を持つシンプルなアプリケーションです。モジュールおよびビューで利用可能な機能は、モジュールとログイン ユーザーの両方に与えられたロールによって決まります。デモ アプリケーションには 3 人のユーザーがいます。Admin、Guest、および Jane Doe (ユーザー) です。ビジネス ルールにより、Guest アカウントではメイン モジュールにアクセスできません。
このアプリケーションの驚くべき点は、その単純さにあります。ビューの分離コードや UI 要件によって煩雑になるドメイン オブジェクトはありません。ビューモデルは UI 固有の状態を保持し、プレゼンターがビジネス ロジックと DAL を使用して懸案事項に関連するビューまたはビューモデルを管理します。処理の組み立てはボタンのクリックなどのインフラストラクチャによって行われるため、開発者はビジネス ロジックに専念できます。アプリケーションの使用経験がない開発者でも、どこからコードを調べ始めればよいかがすぐにわかります (常にプレゼンターから始めます)。
密結合のコンポーネント
図 1 は、全体が 1 つの密結合になった状態を示しています。この結合は、Prism と DI を使用したモデル - ビュー - プレゼンター ビューモデル (MVPVM: Model-View-Presenter ViewModel) パターンになると想定されます。このパターンでは、モジュールがプレゼンターのインスタンスを作成します。次にプレゼンターが必要なビューとビューモデルのインスタンスを作成して、必要に応じて関連付けます。モジュールとそのコンポーネントは、互いについての情報を持ちません。
図 1 密結合のコンポーネント
理想的には、他のソリューションでもこれらのモジュール (およびこのフレームワーク) を再利用できるように、モジュール (ApplicationController、Main、および Guest) ごとに別々のプロジェクトを作成します。(注: GuestPresenter と MainPresenter はどちらも MainViewModel を共有するため、MainViewModel をどちらからでもアクセスできる共有プロジェクトに移動させる必要もあります。懸案事項の分離や再利用を考え始めると、特に複数のプラットフォームを対象にコーディングしている場合、瞬く間にプロジェクトの数が増えていくことがわかるでしょう)。シンプルなデモにとどめているため、このデモで再利用できるのは、コピーと貼り付けだけだということもわかります。重要なのはバランスをとることで、PCL がそれに役立ちます。
図 2 は、すべての層 (プレゼンテーション、ビジネス、およびデータ) がPCL のリソースを共有するしくみを示しています。セキュリティは、ほぼすべてのアプリケーションの懸案事項になるため、デモ アプリケーションの PCL になる UC0100 にセキュリティとそのエンティティを安全に統合できます。UC0100 が、このデモ アプリケーションが持つ唯一の再利用可能な (そのうえ役立つ) コンポーネントになるだろうと考えています。
図 2 すべての層と PCL の共有リソース
インフラストラクチャの管理に使用する PCL のすべての側面を取り上げると、この記事の範囲を超えてしまうため、図 3 で青く表示しているユース ケースについて説明します。
図 3 PCL のユース ケース
UC0100-020 ベース ユース ケース
エンタープライズ アプリケーションの拡張性を高めるには、プラットフォームを組み立てる一貫性のある基準が役立ちます。これは、開発の初心者だけでなく、しばらくの間モジュールをあまり利用していなかった経験豊富な開発者にも役立ちます。すぐに要求されたタスクを完了させることに取り組め、コードの追跡には最小限の時間しかかかりません。これを念頭に置き、Prism IModule インターフェイス、より具体的には Initialize メソッドを使用するように設計した ModuleBase を作成しました。ここで生じるのは、IModule は Prism (PCL 以外の) アセンブリに存在するため使用できないという問題です。ModuleBase にログの機能を持たせるため、Initialize メソッドが呼び出される前に ILogger インターフェイスにインスタンスを提供する必要があります。図 4 に示すように、ここで ModuleBase は、IModule Initialize メソッドを実装するため、すべてのモジュール、ソリューション、およびプロジェクトのコントラクトとして機能します。
図 4 ModuleBase クラスの Initialize メソッド
public class ModuleBase
{
public ILogger Logger { get; set; }
/// <summary>
/// Called by Prism catalog manager. Provides hook
/// to register types/views and initialize the view model.
/// </summary>
public virtual void Initialize()
{
try
{
// Provide hooks for registrations
RegisterTypes();
RegisterViews();
InitializeModule();
}
catch (Exception ex)
{
Logger.Log("ERROR in [{0}] {1}{2}",
GetType().Name, ex.Message, ex.StackTrace);
}
}
PCL とは異なり、MsdnDemo.Phone プロジェクトは Prism アセンブリへのプラットフォーム固有の参照を持っているため、DI コードはこのプロジェクト内の PresentationModuleBase クラスに常駐し、すべてのモジュールのための基本クラスになります。実際のアプリケーションでは、PresentationModuleBase クラスは、他の DI 基本クラスと同様に、別の再利用可能なプロジェクトに存在することになります。
モジュールが DI コンテナーに解決 (インスタンス化) されるとき、GwnBootstrapper が構成したように Logger が設定されます。Logger のプロパティ値を設定するとき、基本 Logger にインスタンスを渡し、事実上、Initialize (および他の) 仮想メソッドに ModuleBase ILogger 参照を提供します (図 5 参照)。
図 5 Logger プロパティの設定
public class PresentationModuleBase : ModuleBase, IModule
{
[Dependency]
public override ILogger Logger {get;set;}
[Dependency]
public IUnityContainer Container { get; set; }
[Dependency]
public IRegionManager RegionManager { get; set; }
[Dependency]
public IEventAggregator EventAggregator { get; set; }
[Dependency]
public IRegionViewRegistry RegionViewRegistry { get; set; }
(注: Unity フレームワークにおいて、セッターの挿入より前にコンストラクターの挿入が行われるため、ModuleBase のコンストラクターが null になり、ログ記録ステートメントを持てません)。
PresentationModuleBase から派生した次の MainModule コードは 3 つのプラットフォーム (Windows Phone 7、Silverlight、および WPF) すべてに共通するコードまたはファイルです。
public class MainModule : PresentationModuleBase
{
protected override void RegisterViews()
{
base.RegisterViews();
// Instantiate the presenter, which in turn will instantiate
// (resolve) the View and ViewModel
var presenter = Container.Resolve<MainPresenter>();
// Load this presenters view into the MainRegion
RegionViewRegistry
.RegisterViewWithRegion(MvpVm.MainRegion, () => presenter.View);
// Activate the presenters view after module is loaded
RaiseViewEvent(MvpVm.MainModule, presenter.View.GetType().Name,
ProcessType.ActivateView);
}
}
UC0200-050 エンティティ ユース ケース
Plain Old CLR Object (POCO) を使用すれば、再利用性が最大になります。PCL は INotifyPropertyChanged をサポートしていますが、常にこれらのエンティティを XAML で使用するわけではありません。ASP.NET MVC 3 プロジェクトまたは Entity Framework でこれらのエンティティを使用する場合もあります。そのため、図 6 で示すように、UserEntity オブジェクトおよび SecurityEntity オブジェクトは、すべてのプラットフォームのあらゆるアプリケーションで簡単に再利用できるエンタープライズ レベルの POCO クラスになります。
図 6 UserEntity オブジェクトと SecurityEntity オブジェクト
public class UserEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PrimaryEmail { get; set; }
public string Password { get; set; }
public override string ToString()
{
return string.Format("{0} {1} ({2})", FirstName, LastName, Password);
}
}
public class SecurityEntity : ISecurityViewModel
{
private string _login;
public int Id { get; set; }
public bool IsAuthenticated { get; set; }
public IEnumerable<string> Roles { get; set; }
public string Login
{
get { return _login; }
set
{
_login = value;
Id = 0;
IsAuthenticated = false;
Roles = new List<string>();
}
}
public bool IsInRole(string roleName)
{
if (Roles == null)
return false;
return Roles.FirstOrDefault(r => r == roleName) != null;
}
public bool IsInRole(string[] roles)
{
return roles.Any(role => IsInRole(role));
}
}
UserEntity POCO をビューモデルで使用する場合、ラップする必要があります。図 7 の MainViewModel クラスの抜粋で示すように、ビューモデルは通知を生成し、そのデータは内部で POCO に転送されます。
図 7 POCO へのデータ転送
public UserEntity SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
OnPropertyChanged("SelectedUser");
OnPropertyChanged("FirstName");
OnPropertyChanged("LastName");
OnPropertyChanged("Password");
OnPropertyChanged("PrimaryEmail");
}
}
public string FirstName
{
get { return _selectedUser.FirstName; }
set
{
_selectedUser.FirstName = value;
OnPropertyChanged("FirstName");
}
}
FirstName プロパティしか示していませんが、すべての UserEntity プロパティに同様のラッパーを用意します。SelectedUser を更新する場合、必要に応じて UI フィールドを更新するよう XAML に通知するために、すべての UserEntity プロパティについてプロパティ変更通知を生成することになります。
UC0100-060 イベント ユース ケース
Prism の機能であるイベント アグリゲーションは、結び付きのないコンポーネント間で通信を行う優れた方法を提供する一方で、アプリケーションを疎結合にする手段になります。これは、各コンポーネントがサブスクライバーについての情報なしでイベントを簡単に発行できるようにすることで行われます。同様に、コンポーネントは発行元についての一切の情報なしでイベントをサブスクライブし、応答を処理できます。
イベント アグリゲーションによりコードが理解しにくくなると言う人もいるでしょうが、適切なログを使用すれば、実際はその反対です。たとえば、ユーザーに財務値のために使用する通貨の切り替えを可能にする別のモジュールに、ドロップダウン リストを用意する場合を考えてみます。取り組んでいるコンポーネントはこの設定に依存しており、ビューモデル プロパティのために値を計算するのに使用されます。そして、コンポーネントとドロップダウン リストを持つコンポーネント (別のモジュールにある場合もあります) の間にはロジック層があります。値が変わり、コンポーネントに通知されなかった場合、イベント アグリゲーションを使用してデバッグする方が、他の手段を使用して、パス (または複数のパス) を完全にするすべての考えられるロジックの記録をトレースするよりも簡単でしょう。イベント アグリゲーションを使用すると、デバッグするのはイベントを利用する側と発行する側の 2 つだけです。両方のログを記録した場合は (デモ アプリケーションのように)、問題は単一障害点に絞られます。
MsdnDemo.Phone プロジェクトは Prism CompositePresentationEvent に依存しているため、Prism イベントは、MsdnDemo.Phone プロジェクトに存在する必要があります。そのため、Prism イベントは PCL には存在できません。
public class MessageEvent : CompositePresentationEvent<MessageEventArgs>
{
}
イベントが依存している EventArgs は、数多くのエンタープライズ アプリケーションで使用される共通のイベント引数のセットを PCL に持てるため、PCL で適切に機能します。図 8 は、前の MessageEvent によって処理される MessageEventArgs を示します。
図 8 MessageEvent によって処理される MessageEventArgs
public class MessageEventArgs : EventArgs
{
public object Sender { get; set; }
public Enum Type { get; set; }
public string Message { get; set; }
public bool IsError { get; set; }
public bool IsInvalid { get; set; }
public int StatusCode { get; set; }
public int ErrorCode { get; set; }
private Exception _exception;
public Exception Exception
{
get { return _exception; }
set
{
_exception = value;
IsError = true;
}
}
}
UC0100-100 MvpVmBase ユース ケース
コードを再利用する最大のメリットは信頼性です。つまり、MsdnDemo.Phone アプリケーションの場合のように、単体テストがほとんどのコードの機能をチェックしていることが、非常によくあります。これは、現場での時間と相まって、安定した再利用可能なコードになります。その結果、チーム メンバーの速度と信頼性が向上します。新しい要件のためにコードを再利用する必要がある場合はなおさらです。その他のメリットは、コードが作業しやすくなり、新しい機能が莫大な利益を生み、数時間で新しいアプリケーションを組み立てられる場合もあることです。ビュー、ビューモデル、およびプレゼンター (対応するインターフェイスを適用する) を作成するだけで、新しいモジュールをすぐに実行できます。
MsdnDemo.Phone アプリケーションでは、PresentationPresenterBase (プレゼンター基本クラス) を、プレゼンターが使用するビューおよびビューモデルに単純に送る必要があります。図 9 では、両方のプレゼンターが同じビューモデルを共有する方法に注意してください。
図 9 MsdnDemo.Phone プレゼンター
PresentationPresenterBase は、PCL の MvpVmPresenter クラスから派生しています。MvpVmPresenter は、エンタープライズ アプリケーション間で、すべての共有プレゼンターおよびそのビューモデルのコントラクトとして機能します。図 10 に、そのコードを示します。
図 10 PresentationPresenterBase
public class MvpVmPresenter<TView,TViewModel>
: IMvpVmPresenter
where TView: IView
where TViewModel : IMvpVmViewModel
{
public IShell Shell { get; set; }
public TView View { get; set; }
public TViewModel ViewModel { get; set; }
/// <summary>
/// Called when the view is activated (by Application controller)
/// </summary>
public virtual void ViewActivated(ViewEventArgs e){}
}
PresentationModuleBase クラスで以前に参照した ILogger と同様、ILogger、シェル、ビュー、およびビューモデルも、DI 経由で挿入されているため、(インスタンスを基本クラスに渡す) 同じ方法でラップする必要があります。
PresentationPresenterBase は、PresentationModuleBase と同様、Prism および Unity のプラットフォーム固有のアセンブリを参照するため、すべての DI 関連のサービスを処理する役割があります。プロセスが成熟していくにつれて、より大きな役割が PCL の基本クラスに移行される可能性があります。
図 11 で、DI コンテナーが指定したビューモデル (TViewModel) を解決するとき、ビューモデルの ButtonCommand 用にバインドを設定します (99 行目)。これにより、プレゼンターはすべてのボタンのクリックを、図 9 の 56 行目で示した ExecuteButtonCommandHandler を使用して処理できるようになります。
図 11 プレゼンター基本クラス
プレゼンターがボタンのクリックを処理することは (ビューモデルではなく)、"昔ながらの" アーキテクトをプレゼンテーション モデル パターンおよびアプリケーション モデル パターンからモデル - ビュー - プレゼンター (Model-View-Presenter) パターンへ進化する気にさせた多くのメリットの 1 つです。
メイン ビューモデルの場合と同様に、ビューモデルを共有でき、各プレゼンターは必要に合わせて異なる方法でメイン ビューモデルを使用できます。ボタンのクリックをビューモデルに移行していたら、条件付きステートメントを使用することになりました。それは、いずれコードを膨張させ、開発サイクルにないモジュールのためのロジックが新しいコードに影響を及ぼす可能性があるため、回帰テストが必要になるでしょう。ビューとビューモデルをビジネス ロジックからクリーンな状態に保つことで、それらを最大限に再利用できます。
まとめ
PCL は、エンタープライズ レベルのアプリケーションのコントラクトを提供する一方で、マルチターゲット アプリケーションに必要なプロジェクト数を大幅に削減します。PCL 以外のアセンブリを参照できないという制約は、より良いコーディング プラクティスに役立ち、プロジェクトで懸案事項を明確に分離することを可能にします。PCL のこれらのメリットと DI を組み合わせると、時間をかけ (利用可能な単体テストを通じて) テストしてきた、それぞれ独立したプロジェクトを最大限に再利用できるようになり、スケーラビリティ、拡張性、および新たなタスクを完了するチームの速度が高まります。
Bill Kratochvil は、フリーの契約社員であり、医療業界の大手企業の機密プロジェクトに携わる、選り抜きの開発者チームの第一線に立つ技術者兼設計者です。彼自身は、テキサス州のアマリロに拠点を置く Global Webnet LLC に勤務しています。
この記事のレビューに協力してくれた技術スタッフの Christina Helton と David Kean に心より感謝いたします。