検索
連載

第6回 「コマンド」と「MVVMパターン」を理解する連載:WPF入門(3/3 ページ)

ビューとモデルを疎結合するコマンドを解説。さらに、データ・バインディングとコマンドの仕組みを使ったWPFアプリのアーキテクチャ・パターンMVVMを紹介。

Share
Tweet
LINE
Hatena
前のページへ |       

■MVVMパターン

 これまでの説明でも用語として少し出てきたが、WPFによるGUIアプリケーション開発では(特に、アプリケーション規模が大きい場合)、Figure 4に示すような、ビューとモデルの間に「ビューモデル」と呼ばれるものを挟んだ3階層アーキテクチャで作成する場合が多い。このようなアーキテクチャ・パターンを、「MVVM(Model-View-ViewModel)パターン」と呼ぶ。


Figure 4: ビュー、ビューモデル、モデルの3階層構造
ビューモデルからビューへの表示変更の通知は、INotifyPropertyChangedインターフェイスの実装を通じて行われる。逆に、ビューからビューモデルへのコマンド(状態変更)の通知は、ICommandインターフェイスの実装を通じて行われる。

 MVVMパターンは、有名なMVC(Model-View-Controller)パターンと同種のアーキテクチャ・パターンで、WPFの強力なデータ・バインディング機能の利用を前提とした亜種だと考えればいいだろう。

ビューモデルの役割

 MVVMパターンにおいて、ビューモデルは以下の2つの役割を担う。

  • モデルをWPF向けにラッピング
  • ビューから状態を分離

モデルをラッピング

 前回で説明したように、WPFのデータ・バインディングの機能を最大限享受するためには、データ・ソースがINotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)を実装している必要がある。また、前節での説明のとおり、ユーザーの操作に応じて何らかの処理を行う場合には、ICommandインターフェイスを実装したコマンドを利用することになる。さらに、ユーザーからの入力に不整合がないかなどのデータ検証を行うためには、IDataErrorInfoインターフェイス(System.ComponentModel名前空間)を実装する。

 これらのインターフェイスはWPF専用というわけではなく、実際、WindowsフォームやASP.NETでも使われているものではあるが、それでもすべてのモデルがこれらを実装しているわけではない。そこで、モデルをラッピングして、INotifyPropertyChangedインターフェイスによるプロパティ値の変更通知などを実装するのが、ビューモデルの役割の1つである。

ビューから状態を分離

 一般に、GUI部分(=ビュー)のテストは手間がかかりがちなため、ビューは極力小さくとどめたいという要求があり、「ビューから状態を分離する」ということがよく行われる。

 「状態の分離」といわれてもピンと来ないかもしれないが、WPFのコマンドの仕組みを前提として説明するなら、以下のような2つのテストをそれぞれ別のクラスに分離したいということである。

  • 「このボタンを押したらこのコマンドが実行されるはず」というテスト(=ビューに残す部分)
  • 「このコマンドが実行されたらデータ・ソースのここがこう変化するはず(=状態変化)」というテスト(=ビューモデルに追いやる部分)

 この分離が不明瞭(ふめいりょう)な場合、考え得るすべての状態(仮にm個とする)のときに、考え得るすべての入力(仮にn個)を与えてテストを行う必要が生じ、テスト・ケースが膨大な数(m×n個)となる。これに対して、明確に分離すれば、テスト・ケースを少なく(m+n個)抑えられる。

 もともと、「ビューとモデルの疎結合」という考え方も、ビューを小さくとどめるための工夫の1つである。しかし、モデルはさまざまなビューでの利用が(場合によってはコンソール・アプリケーションでも)可能な形で作られるもので、ある特定のビュー上でしか必要のないような状態を記録しておく場所ではない。そこで、このような特定のビュー上でのみ利用する状態を保持する場所として、ビューモデルを利用することになる。

データ・バインディングによる疎結合

 「ビューとモデルの疎結合」や、「ビューからの状態の分離」が可能なのも、Figure 5に示すように、INotifyPropertyChangedインターフェイスやICommandインターフェイスなどの標準的なインターフェイスを介した通知機構のおかげである。


Figure 5: 「ビューからの状態の分離」・「ビューとモデルの疎結合」
この図の「View」は、ビューとなるWPFウィンドウなどを指している。また「State」は、ビューモデルとなるオブジェクトを指している

 このようなインターフェイスを介した通知機構を利用する際、最後に残る問題は、コマンドの登録やPropertyChangedイベントに対するハンドラの登録を誰がどこで行うかという部分である(Figure 5中の赤い枠線内のような処理)。WPFのデータ・バインディングでは、この手の登録作業を、

View.DataContext = ViewModel;


などと書くだけで、フレームワークの内部ですべて自動的に行ってくれるため、非常に手軽になっている(ここでいう「View」とはビューとなるWPFウィンドウなどのことで、「ViewModel」とはビューモデルとなるオブジェクトのこと。詳しくは前回のデータ・バインディングに関する説明を参照されたい)。

ビューモデルの作成方法

 それでは、INotifyPropertyChanged、ICommand、IDataErrorInfoなどのインターフェイスの実装方法について見ていこう。なお、ビューモデルとビューをデータ・バインディングするための前提条件として、ビュー側のコンストラクタに、

this.DataContext = new ViewModel();  // C#の場合
Me.DataContext = New ViewModel()     '  VBの場合


というコードが用意され、XAMLコードに、

<StackPanel>
  <TextBox Text="{Binding X, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
           ToolTip="{Binding RelativeSource ={x:Static RelativeSource.Self},
  Path=(Validation.Errors)[0].ErrorContent}"/>
  <Button Content="OK" Command="{Binding OkCommand}" />
</StackPanel>


というコードがあらかじめ記述されているものとする(このコードにある「Bindingマークアップ拡張」の書き方については、第5回の記事を参照してほしい)。

INotifyPropertyChangedインターフェイスの実装

 INotifyPropertyChangedインターフェイスは、PropertyChangedイベントを持っていて、プロパティの値が変化した際には、このイベントを起こすことになる。例えば、int/Integer型の「X」というプロパティを実装するなら、List 4に示すようなコードを書く。

using System.ComponentModel;

public class ViewModel : INotifyPropertyChanged
{
  private int _x;
  public int X
  {
    get { return _x; }
    set
    {
       if (_x != value)
       {
        _x = value;
        RaisePropertyChanged("X");
       }
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;
 
  protected void RaisePropertyChanged(string propertyName)
  {
    var d = PropertyChanged;
    if (d != null)
      d(this, new PropertyChangedEventArgs(propertyName));
  }
}

Imports System.ComponentModel

Public Class ViewModel : Implements INotifyPropertyChanged

  Private _x As Integer
  Public Property X As Integer
    Get
      Return _x
    End Get
    Set(ByVal value As Integer)
      If _x <> value Then
        _x = value
        RaisePropertyChanged("X")
      End If
    End Set
  End Property

  Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

  Protected Sub RaisePropertyChanged(ByVal propertyName As String)
    RaiseEvent PropertyChanged( _
      Me, New PropertyChangedEventArgs(propertyName))
  End Sub

End Class

List 4: INotifyPropertyChangedインターフェイスを用いたプロパティ値の変更通知の実装(上:C#、下:VB)

 また、「X+Y」の値を返すような、Xプロパティに依存する別のプロパティ(今回の例では「Sum」というプロパティ)を作る場合には、List 5に示すようなコードを書く(追記した個所は太字で示している)。

……省略……

  public int Sum { get { return X + Y; } }

  // ToDo: ここにYプロパティも実装する必要がある

  private int _x;
  public int X
  {
    get { return _x; }
    set
    {
      if (_x != value)
      {
        _x = value;
        RaisePropertyChanged("X");
        RaisePropertyChanged("Sum");
      }
    }
  }
……省略……

……省略……

  Public ReadOnly Property Sum As Integer
    Get
      Return X + Y
    End Get
  End Property

  ' ToDo: ここにYプロパティも実装する必要がある

  Private _x As Integer
  Public Property X As Integer
    Get
      Return _x
    End Get
    Set(ByVal value As Integer)
      If _x <> value Then
        _x = value
        RaisePropertyChanged("X")
        RaisePropertyChanged("Sum")
      End If
    End Set
  End Property
……省略……

List 5: ほかのプロパティに依存するプロパティの値の変更通知(上:C#、下:VB)

ICommandインターフェイス(=コマンド)の実装

 前述のとおり、コマンドの仕組みを使う際にはICommandインターフェイスを実装したクラスを作成する。ただ、コマンド1つ1つに対して毎度クラスを作成するのは手間がかかるので、ヘルパー的なクラスを作っておく場合が多い。

 特によく使われるパターンは、List 6に示すような、ExecuteメソッドやCanExecuteメソッド内ではデリゲートを呼び出すだけというクラスである(DelegateCommandという名前を付けることが多い)。

using System;
using System.Windows.Input;

public class DelegateCommand : ICommand
{
  public Action<object> ExecuteHandler { get; set; }
  public Func<object, bool> CanExecuteHandler { get; set; }

  #region ICommand メンバー

  public bool CanExecute(object parameter)
  {
    var d = CanExecuteHandler;
    return d == null ? true : d(parameter);
  }

  public void Execute(object parameter)
  {
    var d = ExecuteHandler;
    if (d != null)
      d(parameter);
  }

  public event EventHandler CanExecuteChanged;
 
  public void RaiseCanExecuteChanged()
  {
    var d = CanExecuteChanged;
    if (d != null)
      d(this, null);
  }

  #endregion
}

Public Class DelegateCommand : Implements ICommand

  Public Property ExecuteHandler As Action(Of String)
  Public Property CanExecuteHandler As Func(Of Object, Boolean)

#Region "ICommand メンバー"

  Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute
    Dim d = CanExecuteHandler
    Return IIf(d = Nothing, True, d(parameter))
  End Function

  Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute
    Dim d = ExecuteHandler
    If d <> Nothing Then
      d(parameter)
    End If
  End Sub

  Public Event CanExecuteChanged(ByVal sender As Object, ByVal e As EventArgs) Implements ICommand.CanExecuteChanged

  Public Sub RaiseCanExecuteChanged()
    RaiseEvent CanExecuteChanged(Me, Nothing)
  End Sub

#End Region

End Class

List 6: DelegateCommandクラス(上:C#、下:VB)

 このDelegateCommandクラスを用いて、「Xプロパティの値が正のときだけ実行ができ、実行時にはメッセージボックスを表示する」というようなコマンドを実装するなら、ビューモデルのコードはList 7のようになる(List 4から追記した個所は太字で示している。List 5で追記した部分は削除した)。

using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

public class ViewModel : INotifyPropertyChanged
{
  private int _x;
  public int X
  {
    get { return _x; }
    set
    {
      if (_x != value)
      {
        _x = value;
        RaisePropertyChanged("X");
        ((DelegateCommand)OkCommand).
                             RaiseCanExecuteChanged();
      }
    }
  }

  private void OkCommandExecute(object parameter)
  {
    MessageBox.Show("コマンドが実行されました。");
  }

  private bool OkCommandCanExecute(object parameter)
  {
    return X > 0;
  }
 
  private ICommand _okCommand;
  public ICommand OkCommand
  {
    get
    {
      if (_okCommand == null)
        _okCommand = new DelegateCommand
        {
          ExecuteHandler = OkCommandExecute,
          CanExecuteHandler = OkCommandCanExecute,
        };
      return _okCommand;
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  protected void RaisePropertyChanged(string propertyName)
  {
    var d = PropertyChanged;
    if (d != null)
      d(this, new PropertyChangedEventArgs(propertyName));
  }
}

Imports System.ComponentModel

Public Class ViewModel : Implements INotifyPropertyChanged

  Private _x As Integer
  Public Property X As Integer
    Get
      Return _x
    End Get
    Set(ByVal value As Integer)
      If _x <> value Then
        _x = value
        RaisePropertyChanged("X")
        CType(OkCommand, DelegateCommand).
          RaiseCanExecuteChanged()
      End If
    End Set
  End Property

  Private Sub OkCommandExecute(ByVal parameter As Object)
    MessageBox.Show("コマンドが実行されました。")
  End Sub

  Private Function OkCommandCanExecute(ByVal parameter As Object) As Boolean
    Return X > 0
  End Function

  Private _okCommand As ICommand
  Public ReadOnly Property OkCommand As ICommand
    Get
      If _okCommand Is Nothing Then
        _okCommand = New DelegateCommand With
        {
          .ExecuteHandler = _
            AddressOf OkCommandExecute,
          .CanExecuteHandler = _
            AddressOf OkCommandCanExecute
        }
      End If
      Return _okCommand
    End Get
  End Property

  Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged

  Protected Sub RaisePropertyChanged(ByVal propertyName As String)
    RaiseEvent PropertyChanged( _
      Me, New PropertyChangedEventArgs(propertyName))
  End Sub

End Class

List 7: DelegateCommandクラスの利用例(上:C#、下:VB)

IDataErrorInfoインターフェイス(=データ検証)の実装

 前回説明したように、データ・バインディング時にValidatesOnDataErrorsプロパティに「True」を設定することで(Bindingマークアップ拡張は前述のコード例を参照)、IDataErrorInfoインターフェイス(System.ComponentModel名前空間)を用いたデータ検証が有効になる。

 例えば、List 8のようなコードを書くことで、Xプロパティの値が正の数でないときに検証エラーを表示できる。

public class ViewModel : INotifyPropertyChanged, IDataErrorInfo
{
  // XプロパティやPropertyChangedイベントの実装はList 4と同じ
  ……省略……

  #region IDataErrorInfo メンバー

  public string Error
  {
    get { return X <= 0 ? "X は正の数" : null; }
  }

  public string this[string columnName]
  {
    get
    {
      switch(columnName)
      {
        case "X": return X <= 0 ? "X は正の数" : null;
      }
      return null;
    }
  }

  #endregion,
}

Imports System.ComponentModel

Public Class ViewModel : Implements INotifyPropertyChanged, IDataErrorInfo

  ' XプロパティやPropertyChangedイベントの実装はList 4と同じ
  ……省略……

#Region "IDataErrorInfo メンバー"

  Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error
    Get
      Return IIf(X <= 0, "X は正の数", Nothing)
    End Get
  End Property

  Default Public ReadOnly Property Item(ByVal columnName As String) As String Implements IDataErrorInfo.Item
    Get
      Select Case columnName
        Case "X"
          Return IIf(X <= 0, "X は正の数", Nothing)
      End Select
      Return Nothing
    End Get
  End Property

#End Region

End Class

List 8: IDataErrorInfoインターフェイスの実装例(上:C#、下:VB)

 ただし、この例のような、インデクサの中で「switch(columnName)」/「Select Case columnName」で分岐するような実装では、プロパティの数が増えるにつれてコードの管理が大変になる。そこで、例えば以下のリンク先の参考ページのように、データ検証属性(=System.ComponentModel.DataAnnotations名前空間にあるRequired属性などを用いて実装を簡素化するというような方法がよく用いられる(リンク先の参考ページにおけるサンプル・コードはSilverlight 4向けで、そのままではWPFで利用できないが、ほぼ同様の実装が可能である)。

 ちなみに、ここで出てきたINotifyDataErrorInfoインターフェイスは、Silverlightにだけ存在し、データ検証状態が変化したことを通知する仕組みが入ったインターフェイスである(IDataErrorInfoインターフェイスには通知の仕組みがなく、プロパティをまたいだ検証がしづらくなっている)。

【コラム】MVVMパターンを活用する前に知っておきたい補足情報

自動生成

 List 4〜8のコードを見てのとおり、ビューモデルには定型的なコードが大量に並ぶことになるため、実装が非常に面倒という問題がある。そこで、コードの自動生成などに頼り、開発を効率化するための手段がいくつか提供されている。

 参考:

デザイン時属性

 WPFのデータ・バインディングの仕組みは、リフレクションなどを使って動的に動作しているため、ビルド時にスペルミスなどの人的エラーに悩まされたり、Visual Studioの人気機能の1つであるコード補完が働かなかったりといった問題もある。

 そこでVisual Studio 2010やExpression Blendでは、デザイン時限定で適用され、ビルド結果の実行可能ファイル中には情報が残らない「d:DataContext」というプロパティ(=デザイン時DataContext)を使って、データ・バインディングの入力補助を行う仕組みを備えている。


d:DataConetxtプロパティ(=デザイン時DataContext)の活用例

 参考:



 今回で、長かったXAML/WPFの基礎や仕組みについて一通り説明が終わった。次回からは個々のUI要素について説明していく。

 具体的には、GridやStackPanelなどのレイアウト用のパネルについて説明を行っていく予定だ。

「連載:WPF入門」のインデックス

連載:WPF入門

前のページへ |       

Copyright© Digital Advantage Corp. All Rights Reserved.

ページトップに戻る