MSDNマガジン 2009年2月号にある「Model-View-ViewModel デザイン パターンによる WPF アプリケーション」にあるModel-View-ViewModelパターンが素敵です。
ざっくり説明すると…
- Model
通常のクラス。
レガシーなC#やVBで作ったクラスたちです。 - View
XAMLです。大体UserControlです。 - ViewModel
INotifyPropertyChangedインターフェースや、IDataErrorInfoインターフェースを実装したViewに特化したクラスです。 - ViewModelのデータをViewへ表示する仕組み
ViewのDataContextにViewModelを入れてBindingして表示します。
IDataErrorInfoや、ValidationRuleを使って入力値の検証を行います。 - Viewでのボタンクリック等の操作をViewModelに通知する仕組み
Commandを使用します。WPF組み込みのRoutedCommandではなく、Delegateを使うCommandを作って使います。
Commandは、ViewModelのプロパティとして公開して、Bindingでボタン等に関連付けます。
といった感じになります。
基本的に、ViewがViewModelを使い、ViewModelがModelを使うという関係になります。
ViewModelがViewを使ったり、ModelがViewModelを使うといったことは原則ありません。
ということで、ハローワールドアプリケーションを作ってみます。
「WpfMVVMHelloWorld」という名前でWPFアプリケーションを新規作成します。
今回作るのは、TextBoxに人の名前を入力してボタンを押すと、TextBlockに「こんにちは○○さん」と表示されるものにします。名前が未入力の場合は、TextBoxの下に名前の入力を促すメッセージを表示してボタンが押せないようにします。
DelegateCommandの作成
とりあえず、アプリケーション本体を作る前に、Delegateを使うICommandの実装を作成します。
using System;
using System.Windows.Input;
namespace WpfMVVMHelloWorld
{
/// <summary>
/// 実行する処理と、実行可能かどうかの判断を
/// delegateで指定可能なコマンドクラス。
/// </summary>
public class DelegateCommand : ICommand
{
private Action<object> _executeAction;
private Func<object, bool> _canExecuteAction;
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteAction)
{
_executeAction = executeAction;
_canExecuteAction = canExecuteAction;
}
#region ICommand メンバ
public bool CanExecute(object parameter)
{
return _canExecuteAction(parameter);
}
// CommandManagerからイベント発行してもらうようにする
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_executeAction(parameter);
}
#endregion
}
}
こいつは、一回作ったら使いまわすのがいいでしょう。もしくはComposite Application Guidance for WPFにあるDelegateCommand<T>を使うのもいいです。
Modelの作成
次にModelを作成します。といってもこのサンプルでは、Nameプロパティをもつだけのシンプルなクラスです。
namespace WpfMVVMHelloWorld
{
public class Person
{
public string Name { get; set; }
}
}
ViewModelの作成
続いてViewModelを作成します。
こいつが今回のサンプルでは一番大変です。気合を入れていきましょう。
INotifyPropertyChangedの実装
ViewとBindingする際に、プロパティの変更を通知するためにINotifyPropertyChangedを実装します。いつもの定型句なのでさらっとコードだけを示します。
namespace WpfMVVMHelloWorld
{
public class HelloWorldViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged メンバ
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected void OnPropertyChanged(string name)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
#endregion
}
}
次に、内部にModelクラスを保持させるフィールドと、コンストラクタで初期化するコードを書きます。
public class HelloWorldViewModel : INotifyPropertyChanged
{
// ModelクラスであるPersonを保持する。
// コンストラクタでModelを指定するようにしている。
private Person _model;
public HelloWorldViewModel(Person model)
{
_model = model;
}
#region INotifyPropertyChanged メンバ
// 省略
#endregion
}
続いてViewに公開するプロパティを定義します。
定義するプロパティはユーザに入力してもらうためのNameプロパティと、ボタンを押したあとに表示するMessageプロパティの2つになります。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace WpfMVVMHelloWorld
{
public class HelloWorldViewModel : INotifyPropertyChanged
{
#region コンストラクタと、コンストラクタで初期化するフィールド
// 省略
#endregion
#region 入力・出力用プロパティ
// ModelクラスのNameプロパティの値の取得と設定
public string Name
{
get { return _model.Name; }
set
{
if (_model.Name == value) return;
_model.Name = value;
OnPropertyChanged("Name");
}
}
// こちらは通常のプロパティ
private string _message;
public string Message
{
get { return _message; }
set
{
if (_message == value) return;
_message = value;
OnPropertyChanged("Message");
}
}
#endregion
#region INotifyPropertyChanged メンバ
// 省略
#endregion
}
}
どちらのプロパティもPropertyChangedイベントを発行していることに注意してください。
こうすることで、ViewModel内での変更をViewに通知できます。
入力値の検証ロジックの作成
次に、ViewModelに入力値の検証ロジックを追加します。
入力値の検証は、IDataErrorInfoインターフェースを実装して追加します。IDataErrorInfoのthis[string columnName]に、columnNameで指定されたプロパティの検証を行うようなコードを追加します。
検証エラーがあった場合は、エラーメッセージを返します。
さらに、検証の結果で、メッセージ作成ボタン(まだ作ってない)が押せるかどうかを切り替えたいので、CommandManagerにCanExecuteChangedイベントを発行してもらうコードも追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfMVVMHelloWorld
{
public class HelloWorldViewModel : INotifyPropertyChanged, IDataErrorInfo
{
#region コンストラクタと、コンストラクタで初期化するフィールド
// 省略
#endregion
#region 入力・出力用プロパティ
// 省略
#endregion
#region INotifyPropertyChanged メンバ
// 省略
#endregion
#region IDataErrorInfo メンバ
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get
{
try
{
if (columnName == "Name")
{
if (string.IsNullOrEmpty(this.Name))
{
return "名前を入力してください";
}
}
return null;
}
finally
{
// CanExecuteChangedイベントの発行
// (DelegateCommandでのCanExecuteChangedイベントで
// RequerySuggestedイベントに登録する
// 処理を書いてるからこうできます)
CommandManager.InvalidateRequerySuggested();
}
}
}
#endregion
}
}
Commandの作成
次にCommandを作成します。
Commandも単純に、ViewModelのプロパティとして作成します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfMVVMHelloWorld
{
public class HelloWorldViewModel : INotifyPropertyChanged, IDataErrorInfo
{
#region コンストラクタと、コンストラクタで初期化するフィールド
// 省略
#endregion
#region 入力・出力用プロパティ
// 省略
#endregion
#region INotifyPropertyChanged メンバ
// 省略
#endregion
#region IDataErrorInfo メンバ
// 省略
#endregion
#region コマンド
private ICommand _createMessageCommand;
public ICommand CreateMessageCommand
{
get
{
// 作成済みなら、それを返す
if (_createMessageCommand != null) return _createMessageCommand;
// 遅延初期化
// 今回は、処理が単純なのでラムダ式で全部書いたが、通常は
// ViewModel内の別メソッドとして定義する。
_createMessageCommand = new DelegateCommand(
param => this.Message = string.Format("こんにちは{0}さん", this.Name),
param => ((IDataErrorInfo)this)["Name"] == null);
return _createMessageCommand;
}
}
#endregion
}
}
最初に作成したDelegateCommandを使っています。
こんにちは○○さんというメッセージの作成と、入力値にエラーが無ければ実行可能になるように、ラムダ式で処理をDelegateCommandにお願いしています。
Viewの作成
ついにViewの作成です。HelloWorldViewという名前で、ユーザコントロールを作成します。
このViewには、DataContextにHelloWorldViewModelが入る前提でXAMLを書いていきます。
<UserControl x:Class="WpfMVVMHelloWorld.HelloWorldView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical">
<TextBlock Text="名前:" />
<!--^Nameプロパティのバインド、即座に変更がViewModelに通知されるようにする -->
<TextBox Name="textBoxName"
Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<!-- あればエラーメッセージを標示する -->
<TextBlock
Text="{Binding ElementName=textBoxName, Path=(Validation.Errors).CurrentItem.ErrorContent}"
Foreground="Red"/>
<Separator />
<Button Content="Create Message" Command="{Binding CreateMessageCommand}" />
<TextBlock Text="{Binding Message}" />
</StackPanel>
</UserControl>
さくっとね。
結合!!
今までバラバラに作ってきたものを1つにまとめます。
まずは、ViewModelとViewの繋ぎを書きます。これには、DataTemplateを使います。
App.xamlに以下のように追加します。
<Application x:Class="WpfMVVMHelloWorld.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfMVVMHelloWorld"
StartupUri="Window1.xaml">
<Application.Resources>
<!-- ViewModelとViewの関連付け -->
<DataTemplate DataType="{x:Type l:HelloWorldViewModel}">
<l:HelloWorldView />
</DataTemplate>
</Application.Resources>
</Application>
次にメインとなるウィンドウを作成します。
ここには、ContentPresenterを使って、何処にHelloWorldViewModelを表示するかかきます。
<Window x:Class="WpfMVVMHelloWorld.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<ContentPresenter Content="{Binding}" />
</Grid>
</Window>
最後にApp.xamlのStartupUriを消してStartupイベントを定義します。
そこで、Windowの作成と、ViewModelの作成・初期化を行います。
<Application x:Class="WpfMVVMHelloWorld.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfMVVMHelloWorld"
Startup="Application_Startup">
<Application.Resources>
<!-- 中身は省略 -->
</Application.Resources>
</Application>
using System.Windows;
namespace WpfMVVMHelloWorld
{
/// <summary>
/// App.xaml の相互作用ロジック
/// </summary>
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
// ウィンドウとViewModelの初期化
var window = new Window1
{
DataContext = new HelloWorldViewModel(new Person())
};
window.Show();
}
}
}
これで完成です。
実行!
実行すると、以下のような画面が表示されます。
WindowのContentPresenterの表示がDataTemplateが適用されてHelloWorldViewになっているのがわかります。
テキストボックスに何かを入力すると、バリデーションエラーが消えてボタンが押せるようになります。
ボタンを押すと、メッセージも表示されます。
ということで、Model View ViewModelパターンのハローワールドでした。