WPFでは、強力なデータバインディングを活かした設計パターンとしてModel View ViewModelパターンというアプリケーションを設計するうえでの定石となる設計パターンがあります。Model View ViewModelパターンはMVVMパターンと略されます。MVVMパターンは、WPFだけでなくWebアプリ開発や、その他のアプリ開発にも波及していて、それぞれの状況に応じて形を変えて存在しています。 MVVMパターンは、MSDNマガジンの以下の記事をきっかけに世間に認知されるようになりました。
- Model-View-ViewModel デザイン パターンによる WPF アプリケーション : http://msdn.microsoft.com/ja-jp/magazine/dd419663.aspx
また、MicrosoftはオープンソースでPrismというMVVMパターンをサポートするライブラリを提供しています。
ここでは、簡単にMVVMパターンの考えについて説明したあと、Prismの一部の機能を使って実際にMVVMパターンのサンプルプログラムを作成していきたいと思います。
MVVMパターンとは
MVVMパターンは、View(XAML + コードビハインド)とViewModelと呼ばれるModelをViewに適したインターフェースに変換するレイヤと、アプリケーションを記述するModelのレイヤからなります。ViewとViewModel間は、基本的にデータバインディングによって連携を行います。ViewModelはModelの変更を監視したり、必要に応じてModelのメソッドの呼び出しを行います。この関係を図で表すと以下のようになります。
変更通知の仕組み
MVVMパターンの、変更通知や双方向データバインディングのViewModelからView方向の変更通知にはINotifyPropertyChangedインターフェースを実装したクラスを使用します。INotifyPropertyChangedインターフェースはPropertyChangedイベントのみをもつシンプルなインターフェースです。このイベントを通じてModelからViewModel、ViewModelからViewへの変更通知が行われます。
INotifyPropertyChangedインターフェースの実装をすべてのプロパティに実装するのは負荷が高いため、一般的に以下のようなヘルパークラスが作成されます。
using System.ComponentModel; using System.Runtime.CompilerServices; namespace MVVMSample01 { public class BindableBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null) { if (Equals(field, value)) { return false; } field = value; var h = this.PropertyChanged; if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); } return true; } } }
このクラスを継承すると、プロパティの変更通知機能を持ったクラスが以下のように簡単に作成できます。
namespace MVVMSample01 { public class Person : BindableBase { private int age; public int Age { get { return this.age; } set { this.SetProperty(ref this.age, value); } } private string name; public string Name { get { return this.name; } set { this.SetProperty(ref this.name, value); } } } }
単一のクラスの変更通知はINotifyPropertyChangedインターフェースで行いますが、コレクションの変更通知には、これまでも使ってきたObservableCollection
ユーザーからの入力の処理
ボタンをクリックするなどのユーザーの処理をViewからViewModelに伝えるには、Commandを使用します。この時使用するCommandはRoutedCommandではなく、デリゲートにExecuteとCanExecute処理を移譲する実装のDelegateCommand(RelayCommandという名前で作られることも多いです)クラスを使用します。DelegateCommandをViewModelクラスのプロパティとして定義して、ViewのButtonやMenuItemなどのCommandプロパティとバインドして使用します。
最初のアプリケーションの作成
各レイヤの連携方法がわかったので、簡単なアプリケーションを作成します。このアプリケーションは、入力した文字列を、ボタンを押したタイミングで大文字に変換して出力するものです。ボタンは、入力が空の場合は押すことができません。また、このサンプルプログラムは、処理が単純すぎるためModelに該当する部分は存在しません。あくまでViewとViewModelが連携した場合の動きを示すものです。
WPFアプリケーションを作成してNuGetでPrism.Mvvmのパッケージを追加します。Prism.MvvmはBindableBaseクラスやDelegateCommandクラスなどのMVVMパターンに必須のクラスだけを持ったシンプルなライブラリです。
ライブラリを追加したら、ViewModelを作成します。MainWindowViewModelという名前でクラスを作って以下のようなコードを作成します。入力用のプロパティと出力用のプロパティと変換用のコマンドを定義しています。コマンドの実行可否は、入力値が変化するたびに評価が必要なのでDelegateCommandのCanExecuteを再評価するためのメソッドを呼び出しています。
クラスを定義します。
using Microsoft.Practices.Prism.Commands; using Microsoft.Practices.Prism.Mvvm; namespace MVVMSample01 { public class MainWindowViewModel : BindableBase { } }
入力、出力を受け取るプロパティを定義します。
private string input; /// <summary> /// 入力値 /// </summary> public string Input { get { return this.input; } set { this.SetProperty(ref this.input, value); // 入力値に変かがある度にコマンドのCanExecuteの状態が変わったことを通知する this.ConvertCommand.RaiseCanExecuteChanged(); } } private string output; /// <summary> /// 出力値 /// </summary> public string Output { get { return this.output; } set { this.SetProperty(ref this.output, value); } }
そして、Commandを定義します。
/// <summary> /// 変換コマンド /// </summary> public DelegateCommand ConvertCommand { get; private set; } public MainWindowViewModel() { // 変換コマンドに実際の処理をわたして初期化 this.ConvertCommand = new DelegateCommand( this.ConvertExecute, this.CanConvertExecute); } /// <summary> /// 大文字に変換 /// </summary> private void ConvertExecute() { this.Output = this.Input.ToUpper(); } /// <summary> /// 何か入力されてたら実行可能 /// </summary> /// <returns></returns> private bool CanConvertExecute() { return !string.IsNullOrWhiteSpace(this.Input); }
ビルドしてView(XAML)を作成します。ViewModelをXAMLで参照できるように名前空間の定義を行います。
xmlns:l="clr-namespace:MVVMSample01"
そして、DataContextプロパティに先ほど作成したViewModelクラスを設定します。
<Window.DataContext> <l:MainWindowViewModel /> </Window.DataContext>
画面を作成していきます。入力用のTextBoxと出力用のTextBlockとコマンドを実行するためのButtonを置いて、ViewModelの対応するプロパティとバインディングしています。
<Window x:Class="MVVMSample01.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:l="clr-namespace:MVVMSample01" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <l:MainWindowViewModel /> </Window.DataContext> <StackPanel> <TextBox Text="{Binding Input, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <Button Content="Convert" Command="{Binding ConvertCommand}" /> <TextBlock Text="{Binding Output}" /> </StackPanel> </Window>
実行すると、以下のような画面が表示されます。
TextBoxに文字を入力するとConvertボタンが押せるようになり、押すとConvertボタンの下に大文字に変換された結果が表示されます。
過去記事
- WPF4.5入門 その1 「はじめに」
- WPF4.5入門 その2 「WPFとは」
- WPF4.5入門 その3 「Hello world」
- WPF4.5入門 その4 「Mainメソッドはどこにいった?」
- WPF4.5入門 その5 「全てC#でHello world」
- WPF4.5入門 その6 「WPFを構成するものを考えてみる」
- WPF4.5入門 その7 「XAMLのオブジェクト要素と名前空間」
- WPF4.5入門 その8 「オブジェクト要素のプロパティ」
- WPF4.5入門 その9 「コレクション構文」
- WPF4.5入門 その10 「コンテンツ構文」
- WPF4.5入門 その11 「マークアップ拡張」
- WPF4.5入門 その12 「その他のXAMLの機能」
- WPF4.5入門 その13 「簡単なレイアウトを行うコントロール」
- WPF4.5入門 その14 「レイアウトコントロールのCanvasとStackPanel」
- WPF4.5入門 その15 「レイアウトコントロールのDockPanelとWrapPanel」
- WPF4.5入門 その16 「ViewBoxコントロール」
- WPF4.5入門 その17 「ScrollViewerコントロール」
- WPF4.5入門 その18 「Gridコントロール part 1」
- WPF4.5入門 その19 「Gridコントロール part 2」
- WPF4.5入門 その20 「レイアウトに影響を与えるプロパティ」
- WPF4.5入門 その21 「WPFのコンセプトと重要な機能つまみ食い」
- WPF4.5入門 その22 「Buttonコントロール」
- WPF4.5入門 その23 「DataGridコントロール その1」
- WPF4.5入門 その24 「DataGridコントロール その2」
- WPF4.5入門 その25 「TreeViewコントロール その1」
- WPF4.5入門 その26 「TreeViewコントロール その2」
- WPF4.5入門 その28 「Calendarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その29 「ContextMenuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その30「Menuコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その31 「ToolBarコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その32 「CheckBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その33 「ComboBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その34 「ListBoxコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その35 「RadioButtonコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その36 「Sliderコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その37 「TabControl」 - かずきのBlog@hatena
- WPF4.5入門 その38 「ファイルダイアログ」 - かずきのBlog@hatena
- WPF4.5入門 その39 「情報を表示するコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その40 「Popup、ToolTip、TextBox、Image、MediaElementコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その41 「DispatcherObject」 - かずきのBlog@hatena
- WPF4.5入門 その42 「WPFのプロパティシステム」 - かずきのBlog@hatena
- WPF4.5入門 その43 「読み取り専用の依存関係プロパティ」 - かずきのBlog@hatena
- 拡張されたプロパティメタデータ - かずきのBlog@hatena
- WPF4.5入門 その45 「添付プロパティ」 - かずきのBlog@hatena
- WPF4.5入門 その46 「WPFのイベントシステム」 - かずきのBlog@hatena
- WPF4.5入門 その47 「コンテンツモデル」 - かずきのBlog@hatena
- WPF4.5入門 その48 「WPFのアニメーション その1」 - かずきのBlog@hatena
- WPF4.5入門 その49 「WPFのアニメーション その2」 - かずきのBlog@hatena
- WPF4.5入門 その50 「Style」 - かずきのBlog@hatena
- WPF4.5入門 その51 「リソース」 - かずきのBlog@hatena
- WPF4.5入門 その52 「コントロールテンプレート」 - かずきのBlog@hatena
- WPF4.5入門 その53 「ユーザーコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その54 「カスタムコントロール」 - かずきのBlog@hatena
- WPF4.5入門 その55 「Binding その1」 - かずきのBlog@hatena
- WPF4.5入門 その56「コレクションのバインディング」 - かずきのBlog@hatena
- WPF4.5入門 その57「コマンド」 - かずきのBlog@hatena
- WPF4.5入門 その58「Behavior」 - かずきのBlog@hatena
- WPF4.5入門 その59「Behaviorの自作」 - かずきのBlog@hatena