Cutting Edge
MVP パターンによる Web フォームの強化
Dino Esposito
モデル - ビュー - コントローラー (MVC: Model-View-Controller) パターンの登場は、ソフトウェア開発においては重大な出来事です。MVC は、懸案事項を切り分けてアプリケーションを設計することで、開発プロセスが改善され、完成したアプリケーションが強化されることを示しました。また、パターンを実現する再現性のあるアプローチももたらしました。
しかし、MVC は完全ではなかったため、長年の間にいくつかのバリエーションも登場しました。
MVC は 1980 年代に考案されたため、Web の開発に直接適応しないという問題が表面化しました。MVC を Web に適応させるのにさらに数年かかり、Model2 など、より具体的な MVC パターンが開発されました (Model2 は、Castle MonoRail と ASP.NET MVC で実装された、MVC の実際のバリエーションです)。
さらに一般的な状況では、MVC から進化したモデル - ビュー - プレゼンター (MVP: Model-View-Presenter) パターンがあり、これは、ビューとモデルの間に仲介役としてコントローラーを配置することにより、ビューとモデルを明確に分離します。図 1 に、MVP パターンを使用して設計されたアプリケーションの動作を示します。
図 1 MVP パターンの使用
今月のコラムでは、まず、ASP.NET Web フォームに使用できる (比較的標準の) MVP パターンの実装を紹介し、MVP パターンの適用と MVP パターンがチームに与えるメリットについて説明します。次に、MVP パターンが Windows Presentation Foundation (WPF) と Silverlight に実装されたので、MVP パターンと、ASP.NET MVC およびモデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) を比較します。
MVP の概要
MVP は、オリジナルの MVC から派生したパターンで、1990 年代に Taligent 社 (現在は IBM の一部) で開発されました。ダウンロード可能な資料 (wildcrest.com/Potel/Portfolio/mvp.pdf、英語) では、MVP についてうまくまとめられており、背景にある考えが説明されています。
MVP の考案者は、モデル (ビューで機能することになるデータ) をビューとコントローラーのペアから明確に分離しました。また、コントローラーの役割はユーザーとアプリケーションの仲介役であるという MVP パターンの考えを強めるために、名前をコントローラーからプレゼンターに変更しました。プレゼンターは、ユーザーに UI を "提示" し、ユーザーからコマンドを受け取るコンポーネントです。プレゼンターはプレゼンテーション ロジックの大半を含んでおり、ビューとシステムの残りの部分 (バックエンドのサービスやデータ層など) の扱い方を認識します。
MVP の技術革新の中で重要な点は、ビューの詳細が 1 つのインターフェイス (基本クラス) に抽象化されることです。プレゼンターは抽象化したビューと対話するため、プレゼンター自体は再利用性の高い、テストが容易なクラスになります。これにより、2 つの興味深いシナリオが可能になります。
1 つは、プレゼンテーション ロジックが、使用されている UI テクノロジに依存しないことです。そのため、Windows プレゼンテーション層と Web プレゼンテーション層で同じコントローラーを再利用できます。最終的には、プレゼンターはインターフェイスに対してコーディングされるため、インターフェイスの先にあるのが、Windows フォーム オブジェクトでも、ASP.NET ページ オブジェクトでも、WPF Window オブジェクトでも、そのインターフェイスを公開する任意のオブジェクトと対話できます。
もう 1 つは、同じプレゼンターが、同じアプリケーションの異なるビューを操作できることです。これは、サービスとしてのソフトウェア (SaaS: Software as a Service) のシナリオに関する重要な成果です。SaaS のシナリオでは、アプリケーションは Web サーバーでホストされ、ユーザーにはサービスとして提供されます。このとき、各ユーザーにはそれぞれ独自にカスタマイズされた UI が必要になります。
当然のことながら、両方のメリットがすべての状況に当てはまるわけではありません。これらのメリットを享受できるかどうかは、主に、Windows と Web フロントエンドで使用するアプリケーションとナビゲーション ロジックによって異なります。ただし、ロジックが同じであれば、MVP モデルを介してそのロジックを再利用できます。
MVP の動作
MVP パターンを実装する際は、まず、必要なビューごとに抽象化を定義します。ASP.NET アプリケーションのページと、Windows (または WPF や Silverlight) アプリケーションのフォームには、それぞれ、プレゼンテーション層の残りの部分と対話するための独自のインターフェイスがあります。このインターフェイスによって、ビューがサポートするデータ モデルが特定されます。プラットフォームに関係なく、論理的に等価なビューのインターフェイスは同じになります。
ビューを抽象化するときに、ビューが認識し、操作するモデルを組み込みます。その結果、プレゼンターとビュー間の対話をスムーズにするのに役立つアドホックのメソッドとイベントをいくつか用意して、このモデルを拡張できます。図 2 に、ビューの抽象化として考えられる例を示します。この例では、単純なタスク一覧アプリケーションで使用されているビュー (図 3 参照) を抽象化しています。
図 2 ビューの抽象化の例
public interface IMemoFormView {
String Title { get; set; }
String Summary { get; set; }
String Location { get; set; }
String Tags { get; set; }
DateTime BeginWithin { get; set; }
DateTime DueBy { get; set; }
String Message { get; set; }
Int32 GetSelectedPriorityValue();
void FillPriorityList(Int32 selectedIndex);
Boolean Confirm(String message, String title);
void SetErrorMessage(String controlName);
}
図 3 には、このインターフェイスの各メンバーがフォームに表示される要素とどのように対応するかを示します。
図 3 表示要素とインターフェイス メンバーの関係
重要な点は、プレゼンターと UI との対話はすべて、ビューのコントラクトを介して行う必要があることです。ボタンのクリック操作、選択、入力はすべて、プレゼンターに転送し、プレゼンターで処理しなければなりません。プレゼンターがビューの一部のデータをクエリしたり、ビューにデータを渡したりする必要がある場合、インターフェイスにはその処理を行うメソッドが必要です。
ビュー コントラクトを実装する
ビューを表すインターフェイスは、ビュー自体を表すクラスに実装する必要があります。既に説明したように、ビュー クラスは、ASP.NET ではページ、Windows フォームではフォーム、WPF ではウィンドウ、Silverlight ではユーザー コントロールです。図 4 に、Windows フォームの例を示します。
図 4 ビュー クラスの実装例
public partial class MemoForm : Form, IMemoFormView {
public string Title {
get { return memoForm_Text.Text; }
set { memoForm_Text.Text = value; }
...
}
public DateTime DueBy {
get { return memoForm_DueBy.Value; }
set { memoForm_DueBy.Value = value; }
}
public int GetSelectedPriorityValue() {
var priority =
memoForm_Priority.SelectedItem as PriorityItem;
if (priority == null)
return PriorityItem.Default;
return priority.Value;
}
public void FillPriorityList(int selectedIndex) {
memoForm_Priority.DataSource =
PriorityItem.GetStandardList();
memoForm_Priority.ValueMember = "Value";
memoForm_Priority.DisplayMember = "Text";
memoForm_Priority.SelectedIndex = selectedIndex;
}
public void SetErrorMessage(string controlName) {
var control = this.GetControlFromId(controlName);
if (control == null)
throw new NullReferenceException(
"Unexpected null reference for a form control.");
memoForm_ErrorManager.SetError(control,
ErrorMessages.RequiredField);
}
...
}
ご覧のとおり、プロパティは、ビジュアル コントロールの一部のプロパティのラッパーとして実装されています。たとえば、Title プロパティは、TextBox コントロールの Text プロパティのラッパーです。同様に、DueBy プロパティは、DatePicker コントロールの Value プロパティのラッパーです。さらに重要なのは、インターフェイスが、特定のプラットフォーム向けの UI の詳細をプレゼンター クラスから隠ぺいする点です。IMemoFormView インターフェイスと対話するために作成されたプレゼンター クラスは、IMemoFormView インターフェイスを実装する任意のオブジェクトを処理することができ、基盤となるコントロールのプログラミング インターフェイスの詳細を意識する必要はまったくありません。
では、ドロップダウン リストなどのデータのコレクションが必要な UI 要素を処理するには、どうすればよいでしょう。データ バインドを使用すべきでしょうか (図 4 参照)。それとも、ビューをパッシブにして、プレゼンテーション ロジックのない、よりシンプルな手法を選ぶべきでしょうか。
どうするかは皆さんにお任せします。この種の疑問への対応として、MVP パターンは 2 つの独立したパターンに分かれています。この 2 つは、監視コントローラー (Supervising Controller) パターンとパッシブ ビュー (Passive View) パターンで、主な違いはビューのコード量だけです。データ バインドを使用して UI にデータを設定する (図 4 参照) と、プレゼンテーション ロジックの一部がビューに追加され、監視コントローラーのパターンに近くなります。
ビューのロジックが増えると、テストについての注意がさらに必要になります。その上、UI のテストは、容易に自動化できない作業です。監視コントローラーのパターンを使用するか、機能の少ないビューにするかは、皆さんしだいです。
プレゼンター クラス
ビュー内のコントロールは、ユーザーが行う操作をキャプチャし、ボタンのクリック操作やインデックスの選択変更といったビューに対するイベントをトリガーします。ビューにはシンプルなイベント ハンドラーが含まれ、そのビューを管理するプレゼンターへの呼び出しをディスパッチします。ビューは、初めて読み込まれるときに、プレゼンター クラスのインスタンスを作成し、そのインスタンスをプライベート メンバーとして内部に保存します。図 5 に、Windows フォームの代表的なコンストラクターを示します。
図 5 MVP フォームの作成
public partial class Form1 :
Form, ICustomerDetailsView {
private MemoFormPresenter presenter;
public Form1() {
// Framework initialization stuff
InitializeComponent();
// Instantiate the presenter
presenter = new MemoFormPresenter(this);
// Attach event handlers
...
}
private void Form1_Load(
object sender, EventArgs e) {
presenter.Initialize();
}
...
}
通常、プレゼンター クラスは、コンストラクターを介してビューへの参照を受け取ります。ビューはプレゼンターへの参照を保持し、プレゼンターはビューへの参照を保持します。ただし、プレゼンターはコントラクト経由でのみビューを認識します。プレゼンターは、受け取るすべてのビュー オブジェクトを、コントラクトが定義されたビュー インターフェイスに分離することによって機能します。図 6 に、プレゼンター クラスの基本を示します。
図 6 サンプル プレゼンター クラス
public class MemoFormPresenter {
private readonly IMemoFormView view;
public MemoFormPresenter(IMemoFormView theView) {
view = theView;
context = AppContext.Navigator.Argument
as MemoFormContext;
if (_context == null)
return;
}
public void Initialize() {
InitializeInternal();
}
private void InitializeInternal() {
int priorityIndex = _context.Memo.Priority;
if (priorityIndex >= 1 && priorityIndex <= 5)
priorityIndex--;
else
priorityIndex = 2;
if (_context.Memo.BeginDate.HasValue)
_view.BeginWithin = _context.Memo.BeginDate.Value;
if (_context.Memo.EndDate.HasValue)
_view.DueBy = _context.Memo.EndDate.Value;
_view.FillPriorityList(priorityIndex);
_view.Title = _context.Memo.Title;
_view.Summary = _context.Memo.Summary;
_view.Tags = _context.Memo.Tags;
_view.MemoLocation = _context.Memo.Location;
}
...
}
コンストラクターは、ビューへの参照を受け取ってキャッシュし、コントラクトで表されるパブリック インターフェイスを使用してビューを初期化します。図 6 のコードで使用したコンテキスト オブジェクトは、ビューを初期化するためにプレゼンターが呼び出し側から受け取る必要がある入力データです。こうした情報がすべての状況で必要なわけではありませんが、フォームを使用して一部のデータを編集するとき、または一部の情報を表示するダイアログ ボックスがあるときに必要になります。
ビューの初期化は、クラスのメンバーに値を代入する程度の処理です。ただし、この代入によって UI が更新されるようになります。
また、プレゼンター クラスには、UI からの任意の要求に応答して実行される多数のメソッドが含まれます。次のように、クリック操作やユーザー操作は、プレゼンター クラスのメソッドにバインドされます。
private void memoForm_OK_Click(
object sender, EventArgs e) {
presenter.Ok();
}
プレゼンター メソッドは、ビュー参照を使用して入力データにアクセスし、同じ方法で UI を更新します。
MVP でのナビゲーション
プレゼンターには、アプリケーション内のナビゲーションという役割もあります。特に、プレゼンターは、サブビューを有効 (または無効) にし、次のビューへのコマンド ナビゲーションを有効 (または無効) にする役割があります。
サブビューは、実質的にそのビューのサブセットで、通常、コンテキストに応じて展開または拡張できるパネルか、モーダルまたはモードレスの子ウィンドウです。プレゼンターは、ビュー インターフェイスのメンバー (主にブール型のメンバー) を使用してサブビューの表示を管理します。
別のビュー (およびプレゼンター) へのコントロールの転送についてはどうでしょう。アプリケーション コントローラーを表す静的クラス (つまり、次のビューを決定するすべてのロジックを保持する中央コンソール) を作成します。図 7 に、アプリケーション コントローラーの図を示します。
図 7 アプリケーション コントローラー
アプリケーション コントローラー クラスは、他の場所に移動するためにプレゼンターが呼び出すシェルを表します。このクラスには、NavigateTo メソッドがあり、次のビューを決定したり、単に指定されたビューに移動したりするワークフローを実装します。ワークフローはどのようにもできます。実際のワークフローのように複雑にすることも、単なる IF ステートメントのシーケンスにすることもできます。ワークフローのロジックは、アプリケーション コントローラー内で静的にコーディングすることも、外部のプラグ可能なコンポーネントからインポートすることもできます (図 8 参照)。
図 8 アプリケーション コントローラーの実装
public static class ApplicationController {
private static INavigationWorkflow instance;
private static object navigationArgument;
public static void Register(
INavigationWorkflow service) {
if (service == null)
throw new ArgumentNullException();
instance = service;
}
public static void NavigateTo(string view) {
if (instance == null)
throw new InvalidOperationException();
instance.NavigateTo(view);
}
public static void NavigateTo(
string view, object argument) {
if (instance == null)
throw new InvalidOperationException();
navigationArgument = argument;
NavigateTo(view);
}
public static object Argument {
get { return navigationArgument; }
}
}
ワークフロー コンポーネントでの実際のナビゲーション ロジックでは、プラットフォーム固有のソリューションを使用して別のビューに切り替えます。Windows フォームの場合は、メソッドを使用してフォームを開き、表示します。ASP.NET では、Response オブジェクトの Redirect メソッドを使用します。
MVP と ASP.NET MVC
ASP.NET MVC は MVC パターンのバリエーションで、MVP と共通する点がいくつかあります。MVC でのコントローラーは、ビューとバックエンド間の仲介役です。コントローラーはビューへの参照を保持しませんが、モデル オブジェクトにデータを設定し、ビュー エンジンという中間コンポーネントのサービスを使用してビューにモデル オブジェクトを渡します。
ある意味、構造がビューとその UI の特性を反映するモデルを使用して、ビューが抽象化されます。ナビゲーションはコントローラーによって管理され、そのコントローラーは各動作のコンテキストから次のビューを選択します。この処理は、組み込みのロジックを使用して実行されます。特定のコントローラー メソッドのロジックが特に複雑だったとしても (率直に言って、毎日起こることではありません)、選択する次のビューを決定するワークフロー コンポーネントをいつでも導入できます。
Web フォームの場合はどうでしょう。Web フォームは、MVP 実装をホストするのに役立ちます。ただし、ポストバック イベントのコンテキスト内に層を追加することしかできないことを明確にしておく必要があります。ポストバック イベントの前に何かを組み込むことはできません。また、ポストバック イベントの後に何かを行わせることもできません。Web フォームでは、ライフサイクル全体に対応するように拡張される完全な MVP 実装は不可能ですが、ポストバック イベントに関連して MVP を追加するだけでも十分で、これによって Web フォーム ページのテストの容易性が大幅に向上します。
MVP と MVVM
WPF アプリケーションや Silverlight アプリケーションのコンテキストでの MVP と MVVM についてはどうでしょう。MVVM は MVP のバリエーションで、プレゼンテーション モデルとも呼ばれます。考え方は、ビュー モデルがプレゼンター クラスに組み込まれ、ビューが読み書きを行うパブリック メンバーをプレゼンター クラスが公開するというものです。これは、双方向のデータ バインドによって行われます。最終的には MVVM は、リッチ UI や、この機能を容易にする (WPF などの) フレームワークに特に適した、MVP の特殊なバリエーションと考えることができます。
MVVM では、ビューは、プレゼンター クラス (ビュー モデル) のプロパティにデータ バインドされます。ユーザーが何か操作を行うと、プレゼンターでこれらのプロパティが更新されます。ユーザーからの要求 (WPF でのコマンド) はすべて、プレゼンター クラスのメソッドを介して処理されます。プレゼンター メソッドが計算する結果はすべてビュー モデルに格納され、ビューへのデータ バインドによって使用できるようになります。WPF と Silverlight では、MVP パターンを手動で実装してもかまいませんが、Blend などのツールを使用すると、データ バインドによって MVVM を簡単かつ効率的に使用できることがわかります。
ポストバック
MVP では、ビューのヒープを管理する方法に関するガイダンスが提供されます。また、MVP を使用すると、アプリケーション コードの複雑さが増すという代償を伴います。ご想像のとおり、こうした代償は、単純なプログラムよりも大規模なアプリケーションでの方が回収しやすいものです。そのため、MVP はアプリケーションだけにはとどまりません。MVP を使用すると、ビューを表すコントラクトに基づいてデザイナーと開発者が並行して作業できます。このことはあらゆる開発シナリオで必常に役に立ちます。MVP によって、プレゼンター クラスがスタンドアロンになり、ビューから分離されます。Web フォームでは、少なくともポストバックを実行するコードのテストの容易性を向上する妥当な方法は、MVP だけです。
Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) です。
この記事のレビューに協力してくれた技術スタッフの Don Smith と Josh Smith に心より感謝いたします。