プレゼンテーションモデル

インターフェースで使用されるGUIコントロールとは独立して、プレゼンテーションの状態と振る舞いを表現します。

別名: アプリケーションモデル

これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる発展)」の一部です。残念ながら、それ以来、他の多くのことが私の注意を引いてしまい、それらをさらに進める時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料はまさに草稿段階であり、再び作業に取り組む時間を見つけるまで、修正や更新は行いません。

GUIは、GUI画面の状態を含むウィジェットで構成されています。GUIの状態をウィジェットに残しておくと、ウィジェットAPIを操作する必要があるため、この状態を取得するのが難しくなり、ビュークラスにプレゼンテーションの振る舞いを配置することが助長されます。

プレゼンテーションモデルは、ビューの状態と振る舞いを、プレゼンテーションの一部であるモデルクラスに引き出します。プレゼンテーションモデルはドメイン層と連携し、ビューでの意思決定を最小限に抑えるインターフェースを提供します。ビューは、すべての状態をプレゼンテーションモデルに格納するか、状態をプレゼンテーションモデルと頻繁に同期します。

プレゼンテーションモデルは複数のドメインオブジェクトと対話する場合がありますが、プレゼンテーションモデルは特定のドメインオブジェクトに対するGUIフレンドリーなファサードではありません。代わりに、プレゼンテーションモデルは、特定のGUIフレームワークに依存しないビューの抽象化と考える方が簡単です。複数のビューが同じプレゼンテーションモデルを利用できますが、各ビューには1つのプレゼンテーションモデルのみが必要です。コンポジションの場合、プレゼンテーションモデルには1つまたは複数の子プレゼンテーションモデルインスタンスが含まれる場合がありますが、各子コントロールにも1つのプレゼンテーションモデルのみが存在します。

プレゼンテーションモデルは、Visual Works Smalltalkのユーザーにはアプリケーションモデルとして知られています。

仕組み

プレゼンテーションモデルの本質は、UIウィンドウのすべてのデータと振る舞いを表す、完全に自己完結型のクラスですが、画面にUIをレンダリングするために使用されるコントロールは含まれていません。ビューは、プレゼンテーションモデルの状態をガラスに投影するだけです。

これを行うために、プレゼンテーションモデルには、ビューのすべての動的情報のデータフィールドがあります。これには、コントロールの内容だけでなく、コントロールが有効かどうかなども含まれます。一般に、プレゼンテーションモデルは、このすべてのコントロール状態(大量になる可能性があります)を保持する必要はありませんが、ユーザーの操作中に変化する可能性のある状態は保持する必要があります。そのため、フィールドが常に有効になっている場合、プレゼンテーションモデルには状態の追加データは存在しません。

プレゼンテーションモデルには、ビューが表示する必要があるデータが含まれているため、コントロールを表示するには、プレゼンテーションモデルをビューと同期する必要があります。この同期は通常、ドメインとの同期よりも厳密である必要があります。画面の同期では不十分であり、フィールドまたはキーの同期が必要です。

もう少し説明するために、作曲家フィールドはクラシックチェックボックスがオンになっている場合にのみ有効になるという実行例の側面を使用します。

図1:クラシックチェックボックスのクリックに関連する構造を示すクラス

図2:オブジェクトがクラシックチェックボックスのクリックにどのように反応するか。

誰かがクラシックチェックボックスをクリックすると、チェックボックスの状態が変更され、ビューの適切なイベントハンドラーが呼び出されます。このイベントハンドラーは、ビューの状態をプレゼンテーションモデルに保存し、プレゼンテーションモデルから自身を更新します(ここでは、粗粒度の同期を想定しています)。プレゼンテーションモデルには、チェックボックスがオンになっている場合にのみ作曲家フィールドが有効になるというロジックが含まれているため、ビューがプレゼンテーションモデルから自身を更新すると、作曲家フィールドコントロールの有効化状態が変更されます。図では、プレゼンテーションモデルには、作曲家フィールドを有効にする必要があるかどうかを specifically に示すプロパティがあることを示しました。もちろん、これはこの場合、isClassicalプロパティの値を返すだけですが、個別のプロパティは重要です。そのプロパティは、プレゼンテーションモデルが作曲家フィールドを有効にするかどうかを決定する方法をカプセル化しているためです。その決定はプレゼンテーションモデルの責任であることを明確に示しています。

この小さな例は、プレゼンテーションモデルのアイデアの本質を示しています。プレゼンテーションの表示に必要なすべての決定はプレゼンテーションモデルによって行われ、ビューは非常にシンプルになります。

プレゼンテーションモデルの最も厄介な部分は、おそらくプレゼンテーションモデルとビュー間の同期でしょう。記述するのは簡単なコードですが、私は常にこの種の退屈な反復コードを最小限に抑えたいと思っています。理想的には、何らかのフレームワークでこれを処理できるはずです。.NETのデータバインディングなどのテクノロジーでいつか実現することを期待しています。

プレゼンテーションモデルでの同期に関して行う必要がある特定の決定は、どのクラスに同期コードを含めるかです。多くの場合、この決定は、主に必要なテストカバレッジのレベルと、選択したプレゼンテーションモデルの実装に基づいています。同期をビューに配置すると、プレゼンテーションモデルのテストでは検出されません。プレゼンテーションモデルに配置すると、プレゼンテーションモデルのビューに依存関係が追加され、結合とスタブが増えます。それらの間にマッパーを追加することもできますが、調整するクラスがさらに増えます。使用する実装を決定する際には、同期コードで障害が発生することはありますが、通常は簡単に見つけて修正できることを覚えておくことが重要です(細粒度の同期を使用しない限り)。

プレゼンテーションモデルの重要な実装詳細は、ビューがプレゼンテーションモデルを参照する必要があるか、プレゼンテーションモデルがビューを参照する必要があるかです。どちらの実装にも長所と短所があります。

ビューを参照するプレゼンテーションモデルは、一般にプレゼンテーションモデルに同期コードを保持します。結果のビューは非常にダムです。ビューには、動的な状態のセッターが含まれており、ユーザーアクションに応じてイベントを発生させます。ビューは、プレゼンテーションモデルをテストするときに簡単にスタブできるようにインターフェースを実装します。プレゼンテーションモデルはビューを観察し、適切な状態を変更してビュー全体をリロードすることでイベントに応答します。その結果、実際のUIクラスを必要とせずに同期コードを簡単にテストできます。

ビューによって参照されるプレゼンテーションモデルは、一般にビューに同期コードを保持します。同期コードは一般に記述が簡単でエラーを簡単に見つけることができるため、ビューではなくプレゼンテーションモデルでテストを実行することをお勧めします。ビューのテストを記述せざるを得ない場合は、ビューにプレゼンテーションモデルに属するコードが含まれているという手がかりになります。同期のテストを優先する場合は、ビュー実装を参照するプレゼンテーションモデルをお勧めします。

いつ使うか

プレゼンテーションモデルは、ビューからプレゼンテーションの振る舞いを引き出すパターンです。そのため、Supervising Controller(スーパーバイジングコントローラー)およびPassive View(パッシブビュー)の代替手段となります。UIなしでテストできるようにしたり、ある形式の複数ビューをサポートしたり、関心の分離をサポートしたりすることで、ユーザーインターフェースの開発が容易になる場合があります。

Passive View(パッシブビュー)およびSupervising Controller(スーパーバイジングコントローラー)と比較して、プレゼンテーションモデルを使用すると、表示に使用されるビューとは完全に独立したロジックを記述できます。また、状態を保存するためにビューに依存する必要もありません。欠点は、プレゼンテーションモデルとビューの間に同期メカニズムが必要になることです。この同期は非常にシンプルですが、必須です。Separated Presentation(分離プレゼンテーション)では同期がはるかに少なくて済み、Passive View(パッシブビュー)ではまったく同期は必要ありません。

例:実行例(ビューがPMを参照)(C#)

これは、プレゼンテーションモデルを使用してC#で開発された実行例のバージョンです。

図3:アルバムウィンドウ。

ドメインモデルから外側に向かって、基本的なレイアウトについて説明します。ドメインはこの例の焦点ではないため、非常に面白くありません。これは本質的に、アルバムのデータを保持する単一のテーブルを持つデータセットです。次に、いくつかのテストアルバムを設定するためのコードを示します。厳密に型指定されたデータセットを使用しています。

public static DsAlbum AlbumDataSet() {
  DsAlbum result = new DsAlbum();
  result.Albums.AddAlbumsRow(1, "HQ", "Roy Harper", false, null);
  result.Albums.AddAlbumsRow(2, "The Rough Dancer and Cyclical Night", "Astor Piazzola", false, null);
  result.Albums.AddAlbumsRow(3, "The Black Light", "Calexico", false, null);
  result.Albums.AddAlbumsRow(4, "Symphony No.5", "CBSO", true, "Sibelius" );
  result.AcceptChanges();
  return result;
}

プレゼンテーションモデルはこのデータセットをラップし、データにアクセスするためのプロパティを提供します。ウィンドウの単一インスタンスに対応する、テーブル全体のプレゼンテーションモデルの単一インスタンスがあります。プレゼンテーションモデルにはデータセットのフィールドがあり、現在どのアルバムが選択されているかも追跡します。

クラス PmodAlbum...

  public PmodAlbum(DsAlbum albums) {
    this._data = albums; 
    _selectedAlbumNumber = 0;
  }
  private DsAlbum _data;
  private int _selectedAlbumNumber;

PmodAlbum は、データセット内のデータにアクセスするためのプロパティを提供します。基本的に、フォームが表示する必要がある情報の各ビットに対してプロパティを提供します。データセットから直接取得される値の場合、このプロパティは非常に単純です。

クラス PmodAlbum...

  public String Title {
    get {return SelectedAlbum.Title;}
    set {SelectedAlbum.Title = value;}
  }
  public String Artist {
    get {return SelectedAlbum.Artist;}
    set {SelectedAlbum.Artist = value;}     
  }
  public bool IsClassical {
    get {return SelectedAlbum.IsClassical;}
    set {SelectedAlbum.IsClassical = value;}            
  }
  public String Composer {
    get {
      return (SelectedAlbum.IsComposerNull()) ? "" : SelectedAlbum.Composer;
    }
    set {
      if (IsClassical) SelectedAlbum.Composer = value;
    }                 
  }
  public DsAlbum.AlbumsRow SelectedAlbum {
    get {return Data.Albums[SelectedAlbumNumber];}
  }

ウィンドウのタイトルはアルバムのタイトルに基づいています。これは別のプロパティを介して提供します。

クラス PmodAlbum...

  public String FormTitle 
  {
    get {return "Album: " + Title;}
  }

作曲家フィールドを有効にする必要があるかどうかを確認するためのプロパティがあります。

クラス PmodAlbum...

  public bool IsComposerFieldEnabled {
    get {return IsClassical;}
  }

これは、パブリックな IsClassical プロパティへの呼び出しに過ぎません。フォームがこれを直接呼び出さないのはなぜか疑問に思うかもしれませんが、これはプレゼンテーションモデルが提供するカプセル化の本質です。PmodAlbum は、そのフィールドを有効にするためのロジックを決定します。それが単にプロパティに基づいているという事実は、プレゼンテーションモデルには知られていますが、ビューには知られていません。

適用ボタンとキャンセルボタンは、データが変更された場合にのみ有効にする必要があります。データセットはこの情報を記録するため、データセットのその行の状態を確認することでこれを提供できます。

クラス PmodAlbum...

  public bool IsApplyEnabled {
    get {return HasRowChanged;}
  }
  public bool IsCancelEnabled {
    get {return HasRowChanged;}
  }
  public bool HasRowChanged {
    get {return SelectedAlbum.RowState == DataRowState.Modified;}
  }

ビューのリストボックスには、アルバムタイトルのリストが表示されます。PmodAlbum はこのリストを提供します。

クラス PmodAlbum...

  public String[] AlbumList {
    get {
      String[] result = new String[Data.Albums.Rows.Count];
      for (int i = 0; i < result.Length; i++)
        result[i] = Data.Albums[i].Title;
      return result;
    }
  }

これで、PmodAlbum がビューに提示するインターフェースの説明は終わりです。次に、ビューとプレゼンテーションモデル間の同期をどのように行うかを見ていきます。同期メソッドをビューに配置し、粗粒度の同期を使用しています。まず、ビューの状態をプレゼンテーションモデルにプッシュするメソッドがあります。

クラス FrmAlbum...

  private void SaveToPmod() {
    model.Artist = txtArtist.Text;
    model.Title = txtTitle.Text;
    model.IsClassical = chkClassical.Checked;
    model.Composer = txtComposer.Text;
  }

このメソッドは非常に単純で、ビューの変更可能な部分をプレゼンテーションモデルに割り当てるだけです。load メソッドはもう少し複雑です。

クラス FrmAlbum...

  private void LoadFromPmod() {
    if (NotLoadingView) {
      _isLoadingView = true;
      lstAlbums.DataSource = model.AlbumList;
      lstAlbums.SelectedIndex = model.SelectedAlbumNumber;
      txtArtist.Text = model.Artist;
      txtTitle.Text = model.Title;
      this.Text = model.FormTitle;
      chkClassical.Checked = model.IsClassical;
      txtComposer.Enabled = model.IsComposerFieldEnabled;
      txtComposer.Text = model.Composer;
      btnApply.Enabled = model.IsApplyEnabled;
      btnCancel.Enabled = model.IsCancelEnabled;
      _isLoadingView = false;
    }
  }
  private bool _isLoadingView = false;
  private bool NotLoadingView {
    get {return !_isLoadingView;}
  }
private void SyncWithPmod() {
  if (NotLoadingView) {
    SaveToPmod();
    LoadFromPmod();
  }
}

ここでの問題は、同期によってフォームのフィールドが更新され、同期がトリガーされるため、無限再帰を回避することです...フラグを使用してこれを防ぎます。

これらの同期メソッドが配置されたら、次の手順は、コントロールのイベントハンドラーで適切な同期ビットを呼び出すだけです。ほとんどの場合、これは簡単で、データが変更されたときに SyncWithPmod を呼び出すだけです。

クラス FrmAlbum...

  private void txtTitle_TextChanged(object sender, System.EventArgs e){
    SyncWithPmod();
  }

より複雑なケースもあります。ユーザーがリスト内の新しいアイテムをクリックすると、新しいアルバムに移動してそのデータを表示する必要があります。

クラス FrmAlbum...

  private void lstAlbums_SelectedIndexChanged(object sender, System.EventArgs e){
    if (NotLoadingView) {
      model.SelectedAlbumNumber = lstAlbums.SelectedIndex;  
      LoadFromPmod();
    }
  }

クラス PmodAlbum...

  public int SelectedAlbumNumber {
    get {return _selectedAlbumNumber;}
    set {
      if (_selectedAlbumNumber != value) {
        Cancel();
        _selectedAlbumNumber = value;
      }
    }
  }

リストをクリックすると、このメソッドは変更を破棄することに注意してください。例を単純にするために、このひどい使いやすさを行いました。フォームは、変更が失われないように、少なくとも確認ボックスを表示する必要があります。

適用ボタンとキャンセルボタンは、プレゼンテーションモデルに何をすべきかを委任します。

クラス FrmAlbum...

  private void btnApply_Click(object sender, System.EventArgs e)    {
    model.Apply();
    LoadFromPmod();
  }
  private void btnCancel_Click(object sender, System.EventArgs e){
    model.Cancel();
    LoadFromPmod();
  }

クラス PmodAlbum...

  public void Apply ()    {
    SelectedAlbum.AcceptChanges();
  }
  public void Cancel() {
    SelectedAlbum.RejectChanges();
  }

ほとんどの動作をプレゼンテーションモデルに移動できますが、ビューにはまだある程度のインテリジェンスが残っています。プレゼンテーションモデルのテストの側面をより良く機能させるためには、もっと移動できると良いでしょう。プレゼンテーションモデルがビューについてより多くを知ることになるという犠牲を払って、同期ロジックをそこに移動することで、プレゼンテーションモデルにさらに移動することができます。

例:データバインディングテーブルの例(C#)

.NET framework でプレゼンテーションモデルを初めて見たとき、データバインディングはプレゼンテーションモデルを簡単に動作させるための優れたテクノロジーを提供しているように見えました。これまでのところ、現在のバージョンのデータバインディングの制限により、最終的には到達するであろう場所から後退しています。データバインディングが非常にうまく機能する分野の1つは読み取り専用データであるため、これと、テーブルがプレゼンテーションモデルの設計にどのように適合するかを示す例を次に示します。

図4:ロックアルバムが強調表示されたアルバムのリスト。

これは単なるアルバムのリストです。追加の動作は、各ロックアルバムの行がコーンシルク色で着色されることです。

他の例とは少し異なるデータセットを使用しています。テストデータのコードを次に示します。

public static AlbumList AlbumGridDataSet() 
{
  AlbumList result = new AlbumList();
  result.Albums.AddAlbumsRow(1, "HQ", "Roy Harper", "Rock");
  result.Albums.AddAlbumsRow(2, "Lemonade and Buns", "Kila", "Celtic");
  result.Albums.AddAlbumsRow(3, "Stormcock", "Roy Harper", "Rock");
  result.Albums.AddAlbumsRow(4, "Zero Hour", "Astor Piazzola", "Tango");
  result.Albums.AddAlbumsRow(5, "The Rough Dancer and Cyclical Night", "Astor Piazzola", "Tango");
  result.Albums.AddAlbumsRow(6, "The Black Light", "Calexico", "Rock");
  result.Albums.AddAlbumsRow(7, "Spoke", "Calexico", "Rock");
  result.Albums.AddAlbumsRow(8, "Electrica", "Daniela Mercury", "Brazil");
  result.Albums.AddAlbumsRow(9, "Feijao com Arroz", "Daniela Mercury", "Brazil");     
  result.Albums.AddAlbumsRow(10, "Sol da Libertade", "Daniela Mercury", "Brazil");  
  Console.WriteLine(result);
  return result;
}

この場合のプレゼンテーションモデルは、内部データセットをプロパティとして公開します。これにより、フォームはデータセット内のセルに直接データをバインドできます。

private AlbumList _dsAlbums;
internal AlbumList DsAlbums {
  get {return _dsAlbums;}
}

強調表示をサポートするために、プレゼンテーションモデルはテーブルにインデックスを付ける追加のメソッドを提供します。

internal Color RowColor(int row) {
  return (Albums[row].genre.Equals("Rock")) ? Color.Cornsilk : Color.White;
}
private AlbumList.AlbumsDataTable Albums {
  get {return DsAlbums.Albums;}
}

このメソッドは単純な例のメソッドに似ていますが、テーブルデータのメソッドはテーブルの一部を選択するためにセル座標を必要とする点が異なります。この場合、必要なのは行番号だけですが、一般的には行番号と列番号が必要になる場合があります。

ここから、Visual Studioに付属の標準データバインディング機能を使用できます。テーブルセルをデータセット内のデータ、およびプレゼンテーションモデルのデータに簡単にバインドできます。

色の動作をさせるのは少し複雑です。これは例の主な目的から少し外れていますが、標準の WinForms テーブルコントロールで行ごとの強調表示を行う方法がないため、全体が複雑になります。一般的に、このニーズに対する答えは、サードパーティのコントロールを購入することですが、私はそれをするには安すぎます。好奇心旺盛な方のために、私が行ったことをここに示します(このアイデアは、主に http://www.syncfusion.com/FAQ/WinForms/ から引用されています)。今後、WinForms の内部構造に精通していることを前提としています。

基本的に、色の強調表示動作を追加する DataGridTextBoxColumn のサブクラスを作成しました。動作を処理するデリゲートを渡すことで、新しい動作をリンクします。

クラス ColorableDataGridTextBoxColumn...

  public ColorableDataGridTextBoxColumn (ColorGetter getcolorRowCol, DataGridTextBoxColumn original)
  {
    _delGetColor = getcolorRowCol;
    copyFrom(original);
  }
  public delegate Color ColorGetter(int row);
  private ColorGetter _delGetColor;

コンストラクターは、元の DataGridTextBoxColumn とデリゲートを受け取ります。ここで本当にやりたいことは、デコレータパターンを使用してオリジナルをラップすることですが、WinForms の多くのクラスと同様に、オリジナルはすべて封印されています。そのため、代わりに、オリジナルのすべてのプロパティをサブクラスにコピーします。読み取りまたは書き込みできない重要なプロパティがある場合は、これは機能しません。今のところここではうまくいくようです。

クラス ColorableDataGridTextBoxColumn...

  void copyFrom (DataGridTextBoxColumn original) {
    PropertyInfo[] props = original.GetType().GetProperties();
    foreach (PropertyInfo p in props) {
      if (p.CanWrite && p.CanRead)
        p.SetValue(this, p.GetValue(original, null), null) ;
    }
  }

幸いなことに、paint メソッドは仮想です(そうでなければ、まったく新しいデータグリッドが必要になります)。これを使用して、デリゲートを使用して適切な背景色を挿入できます。

クラス ColorableDataGridTextBoxColumn...

  protected override void Paint(System.Drawing.Graphics g, System.Drawing.Rectangle bounds, 
    System.Windows.Forms.CurrencyManager source, int rowNum, 
    System.Drawing.Brush backBrush, System.Drawing.Brush foreBrush, 
    bool alignToRight)
  {
    base.Paint(g, bounds, source, rowNum, new SolidBrush(_delGetColor(rowNum)), foreBrush, alignToRight);
  }

この新しいテーブルを配置するには、フォームにコントロールが構築された後、ページの読み込み時にデータテーブルの列を置き換えます。

クラス FrmAlbums...

  private void FrmAlbums_Load(object sender, System.EventArgs e){
    bindData();
    replaceColumnStyles();
  }
  private void replaceColumnStyles() {
    ColorableDataGridTextBoxColumn.ReplaceColumnStyles(dgsAlbums, 
      new ColorableDataGridTextBoxColumn.ColorGetter(model.RowColor));
  }

クラス ColorableDataGridTextBoxColumn...

  public static void ReplaceColumnStyles(DataGridTableStyle grid, ColorGetter del) {
    for (int i = 0; i < grid.GridColumnStyles.Count; i++) {
      DataGridTextBoxColumn old = (DataGridTextBoxColumn) grid.GridColumnStyles[0];
      grid.GridColumnStyles.RemoveAt(0);
      grid.GridColumnStyles.Add(new ColorableDataGridTextBoxColumn(del, old));
    }
  }

動作しますが、私が望むよりもはるかに厄介であることを認めます。実際にこれを行う場合は、サードパーティのコントロールを検討する必要があります。ただし、本番システムでこれを行ったことがあり、正常に機能しました。