Windows Form に比べて、画面のデザイン性を大幅に強化したWPF(Windows Presentation Foundation)ですが、Windows Form技術者がWPFに移行することを考えた場合、結構敷居が高いと感じるのではないでしょうか。
特に WPF は MVVM(Model-View-View Model)というデザインパターン(プログラムを作る上での約束ごと)を推奨としていることから、イベントハンドラを使ったWindows Formの作り方とは大きく異なります。
ただ、DIYプログラミングを考えた場合、MVVMで作る意味はほとんど無く、逆に生産性を下げてしまうのでお勧めはしていません。
しかし、MVVMの本質について理解しておくことは大切だと思うので、簡単なサンプルを使って解説しておこうと思います。
MVVMとは
MVVM(Model - View - ViewModel)はプログラムの構造を表しています。
これは、1つの画面を作成する際、「画面の入出力」、「データ管理/各種処理」、「画面と各種処理の仲介」という3つの役割にクラスを分けることで、画面と処理を分離する作り方になります。
画面と処理を分離すると、プログラム開発の分業がやり易くなります。
例えば、デザイナーは画面のデザインや操作方法だけを意識し、プログラマーは各種処理のプログラミングだけを意識して開発することが可能になるのです。
また、画面と処理が分離することで、仕様変更における修正箇所が必要最小限に抑えられるというメリットもあります。
もしデザインに関する仕様変更であれば、UIを担当するView の部分を修正するだけで済んでしまいます。
Windows Form と WPF における構造の違い
Windows Formの場合、画面にボタンやテキストボックスなどのコントロールを張り付け、そのコントロールに応じたイベントハンドラに処理を記述していきます。
考え方としては、Windows FormでもMVVMのプログラム構造を持たせることは可能ですが、仮にMVVMにしたとしても、画面とイベントハンドラは切っても切れない関係ですから、完全に分離することはできません。
画面を変更するということは、必ずイベントハンドラのどこかを修正する必要が生じますので、デザイナーはC#やVBというプログラミング言語を習得する必要が出てきます。
一方、WPFの場合、画面はXAMLというXMLに似た言語で記述し、XAMLの中から連携するクラスやプロパティを指定するという方法を使うことで、完全に画面(UI)と処理を分離することが可能になります。
世界で一番短いMVVMのサンプル
では、実際にMVVMのサンプルを見ていきましょう。
その前に、前提条件として今回のサンプルプログラムの動きと、登場するクラス、及びクラスの関係について押さえておきたいと思います。
プログラムの仕様
これは、カウントダウンのボタンをクリックすると、画面の値が1つづつ減っていくという単純なプログラムです。
このプログラムを実現するためのクラス構成は次の通りです。
それぞれのクラスが、どの様にインスタンスを生成しているかについても記述しています。
MVVMと一言で言っても、その作り方はプログラムの仕様や設計者の考え方によって異なってきますので、あくまでも一例としてお考え下さい。
他のサイトのMVVMのサンプルには、C#で書かれたMainWindowクラスから、ViewModelのインスタンスを生成している場合もあります。
Viewモデル(XAML)
では、MVVMのView(UIを担当する部分)を見ていきましょう。
以下がXAMLという言語(マークアップ言語)で記述された画面レイアウトの定義です。
Windows Formと同じくWPFもVisual Studio のデザイナー画面を使ってコントロールを張り付けると、画面を描画するためのC#のコードが自動生成されます。
ただ、WindowsFormとは異なり、XAMLは人手での編集が前提になっているという点が異なります。
ちなみに、このサンプルも一部手で追記していますが、詳細はのちほど説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<Window x:Class="MvvmTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MvvmTest" mc:Ignorable="d" Title="MainWindow" Height="174" Width="466"> <Window.DataContext> <local:CounterViewModel/> </Window.DataContext> <Grid Background="#FFFFD5D5"> <TextBox HorizontalAlignment="Left" Height="23" Margin="74,51,0,0" TextWrapping="Wrap" Text="{Binding Value}" VerticalAlignment="Top" Width="139"/> <Button Content="カウントダウン" HorizontalAlignment="Left" Margin="294,51,0,0" VerticalAlignment="Top" Width="75" Command="{Binding CountDownCommand}"/> </Grid> </Window> |
Model、ViewModel
次は、Model、ViewModelの部分になります。
説明の関係上、C#のソースコードをまとめたかったので、画面のインスタンス(MvvmText.MainWindowsクラスのインスタンス)を生成する部分も含めて1つのファイルに記述しています。
実際は、クラスごとにファイルを分けることが一般的です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.ComponentModel; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace MvvmTest { ///============================================================ /// VIEWのインスタンスを生成するためのコード部分 /// VisualStudioから自動生成された状態のまま放置 ///============================================================ /// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } ///============================================================ /// Counterクラス /// ----------------------------------------------------------- /// MVVMのModel部分であり、カウンターの値を保持するクラス。 ///============================================================ /// <summary> /// カウンターの値を保持するクラス /// </summary> public class Counter { public int Value { get; set; } = 100; } ///============================================================ /// CounterViewModelクラス /// ----------------------------------------------------------- /// MVVMのView Model部分であり、View と Model の間を取り持つ ///============================================================ /// <summary> /// MVVMのView Model は、必ずINotifyPropertyChangedを継承するルールがある。 /// </summary> public class CounterViewModel : INotifyPropertyChanged { /// <summary> /// View Modelのルールとして実装しておくイベントハンドラ /// </summary> public event PropertyChangedEventHandler PropertyChanged; //Modelのインスタンスを保持するプロパティ public Counter Counter { get; set; } /// <summary> /// コマンドを格納するプロパティ /// </summary> public CountDownCommand CountDownCommand { get; private set; } /// <summary> /// コンストラクタ /// ここでコマンドのインスタンスを生成し、プロパティに格納しておく /// </summary> public CounterViewModel() { CountDownCommand = new CountDownCommand(this); Counter = new Counter(); } /// <summary> /// 他のクラスから参照/設定が行えるようにするためのプロパティの定義 /// View のXAMLに記述したバインドにより、View Modelからアクセスされる /// </summary> public int Value { get { //カウンタークラスが保持するカウント値を返す return Counter.Value; } set { //カウンタークラスが保持するカウント値を設定する Counter.Value = value; //中身が変更されたことを View Modelに通知するためのイベントハンドラ呼び出し //引数として、プロパティ名を文字列として渡すことでVIEWからバインドされる if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Value")); } } } } ///============================================================ /// CountDownCommand クラス /// ----------------------------------------------------------- /// ViewModel部分から呼び出されるコマンドの中身を定義 ///============================================================ /// <summary> /// MVVMのViewModelから呼び出されるコマンドの定義 /// コマンド1つにつき1つのクラスを定義する /// </summary> public class CountDownCommand : ICommand { /// <summary> /// コマンドを読み出す側のクラス(View Model)を保持するプロパティ /// </summary> private CounterViewModel _view { get; set; } /// <summary> /// コンストラクタ /// コマンドで処理したいクラス(View Modeo)をここで受け取る /// </summary> /// <param name="view"></param> public CountDownCommand(CounterViewModel view) { _view = view; } /// <summary> /// コマンドのルールとして必ず実装しておくイベントハンドラ /// 通常、このメソッドを丸ごとコピーすればOK /// </summary> public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } /// <summary> /// コマンドの有効/無効を判定するメソッド /// コマンドのルールとして必ず実装しておくメソッド /// 有効/無効を制御する必要が無ければ、無条件にTrueを返しておく /// </summary> /// <param name="parameter"></param> /// <returns></returns> public bool CanExecute(object parameter) { return true; } /// <summary> /// コマンドの動作を定義するメソッド /// コマンドのルールとして必ず実装しておくメソッド /// </summary> /// <param name="parameter"></param> public void Execute(object parameter) { _view.Value = (_view.Value <= 0) ? 100 : _view.Value - 1; } } } |
自動生成されるMainWindowには何も書かない
MainWindowは、Windows Form の Form クラスに相当します。
Visual Studioのレイアウトエディタでコントロールを張り付けることで自動生成されるコードであり、InitializeCompornent() の中で、画面に張り付けたコントロールのインスタンスが作成されます。
Windows Formでは、コンストラクタやLoadイベントハンドラ内で、様々なクラスを new します。
しかし、MVVM ではコンストラクタやLoadイベントに何かのクラスを new するコードを書く必要はありません。
1 2 3 4 5 6 7 8 9 10 |
/// <summary> /// MainWindow.xaml の相互作用ロジック /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } |
では、Counter クラスや、それ以外のクラスは何処でインスタンスが生成されるのでしょう?
その答えは、XAML側にあります。
XAMLに次のコードを記述することで、CounterViewModelのインスタンスが生成されるのです。
1 2 3 |
<Window.DataContext> <local:CounterViewModel/> </Window.DataContext> |
WPFの場合、Windowや各種コントロールは DataContext というプロパティを持っており、ここに表示したいオブジェクトを代入します。
その一通りの手順が上記のソースコードになります。
無論、Windows Formと同様、コンストラクタやLoadイベントの中にC#のソースコードで記述することも可能です。
実際、いくつかの入門サイトでは、コンストラクタでクラスを生成するサンプルが掲載されていました。
1 |
this.DataContext = new CounterViewModel(); |
どちらの方法でも良いのですが、今回はXAML上でクラスを生成し、WindowのDataContextに代入するという方法を使いました。
話を元に戻しますが、DataContextに代入したオブジェクトは、XAML側で次のように記述することで、画面の各項目と連携させることが可能になります。
1 2 |
<TextBox HorizontalAlignment=・・・・中略・・・・・・ Text="{Binding Value}" ・・・・中略・・・・・・/> |
連携することで、プログラム側からクラスのプロパティを変更するだけで、画面に値が反映されますし、逆に画面から入力された値が、クラスのプロパティに設定されるようになります。
プログラムで値を変更したら、その事を画面に伝える
連携させると、ソースコードでクラスの値を変更したら、自動的に画面にも反映されると書きましたが、実はその為にもう1手間必要になります。
というのも、いくら連携していると言ってもクラス内に保持している値が変わった事までは画面側で把握できないのです。
つまり、プログラムで Value+=1 という処理を行っても、画面はその事を知る由もなく、画面に反映されません。
そこで、それを画面に通知するための仕組みを記述します。
具体的には、次の3つをクラスに実装します。
- INotifyPropertyChangedの継承
- PropertyChangedEventHandlerイベントハンドラの記述
- プロパティの set 内でのPropertyChangedEventHandlerイベントハンドラの呼び出し
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class CounterViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public int Value { get { ・・・・中略・・・・ } set { ・・・・中略・・・・ if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Value")); } } } } |
このように記述しておくこと、このプロパティを通してクラスの値を変更した際、そのことが画面に通知されるようになります。
プログラムから値を変更すると言っても、その為には何らかのトリガが必要ですよね。
つまり、クラスの中身をプログラムから変更するということは、例えばタイマーを使って一定時間毎とか、画面に設置されているボタンがクリックされた時とか、マウスが移動した時とかです。
画面系のプログラムは、ユーザーの操作に対して結果を返すものであるため、通常は入力やボタンクリック等をアクションがトリガとなります。
MVVMの場合、このトリガ(画面操作)もBindingによってViewModelに関連付けをします。
Commandクラスで処理を実行する
XAMLのボタンに{binding CountDownCommand} という記述がありますが、これがボタンと処理を関連付けるコードになります。
1 2 3 |
<Button Content="カウントダウン" ・・・中略・・・ ・・・中略・・・ Command="{Binding CountDownCommand}"/> |
ボタンクリックに対応した処理は ViewModel に実装し、public で外部から呼び出しが出来る様にしておきます。
その上で、Command={Binding メソッド名} と記述すれば、ボタンクリック時にこのメソッドが呼び出されます。
大まかな流れは次の通りです。
ここでは CountDownCommand クラスが登場しました。
CounterViewModel の役割は画面とModelの仲介役になるので、画面からのアクションはここで受け付けます。
画面の入出力項目とBinding により関連図けるためにはルールを守る必要があり、INotifyPropertyChangedを継承することと、PropertyChangedEventHandler イベントハンドラを実装することを先ほど説明しました。
画面からBindingによりメソッドを呼び出す場合も、守るべきルールが存在します。
それは、次の4つです。
- ICommandを継承する
- CanExecuteChanged イベントハンドラを実装する
- CanExecuteメソッドを実装する
- Executeメソッドを実装する
言い換えると、XAMLの画面からBinding で呼び出せるのは、「これら4つのルールを守って作ったクラスのメソッドのみ」という事です。
CounterViewModel は既にINotifyPropertyChangedクラスを継承しているため、これ以上クラスを継承することは出来ません。(C#は多重継承不可)
なので、外部にCommand専用のクラスを用意し、そのインスタンスを ViewModelに持たせて、public で外部に公開するという手段を取ります。
Commandクラスには3つのメソッドを記述
Commandクラスの中身を見ていきましょう。
基本構造は次の様になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class CountDownCommand : ICommand { public event EventHandler CanExecuteChanged { ・・・・・ } public bool CanExecute(object parameter) { ・・・・・ } public void Execute(object parameter) { ・・・・・ } } |
順番が前後しますが、CanExecuteメソッドは、このコマンドが実行できる状態か否かを呼び出し元に返すメソッドです。
ここで return false; と記述すると、Binding で関連付けられたコントロールは操作が出来ない状態になります。
コントロールの状態をコントロールする必要が無ければ、無条件に return True; と記述します。
CanExecuteChangedイベントハンドラは、MVVMの仕組みの中で呼び出され、CanExecuteメソッドが返す結果に応じて、RequerySuggestedイベントハンドラが追加されたり削除されたりします。
このイベントハンドラの実装は必須ですが、特別な事が無い限り固定で下記の内容を記述しておきます。
1 2 3 4 5 |
public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } |
最後に Execute メソッドですが、ここにコマンドの処理を記述します。
今回のサンプルでは、呼び出される度に値をマイナス1し、0以下になったら100に戻すという処理を行っています。
1 2 3 4 |
public void Execute(object parameter) { _view.Value = (_view.Value <= 0) ? 100 : _view.Value - 1; } |
コマンド1つにつき Commandクラスを1つ用意
今までの説明で気づいたかと思いますが、1つのCommandクラスには、1つの処理しか記述できません。
何故なら、Commandクラスには、Executetと言う決められたメソッドを用意する必要があるからです。
例えば、ファイルの読み込みと書き込みを考えた場合、2つの Commandクラスを用意して、1つはファイル読み込み、もう1つはファイル書き込みのコードを書く事になります。
Parameter の値で処理を条件分岐
コマンドが増えるとCommandクラスも増えていくので、ソースコードがしだいに膨れていきます。
でも、1つのクラスの中で複数の処理を記述すると、プログラム修正の時に影響範囲が大きくなるので痛し痒しです。
ただ、メニューなどで単一機能をたくさん用意して使い分けたり、似たような処理の一部を引数で分岐させたい場合であっても、それぞれCommandを用意する必要があるのでしょうか?
いいえ、そんなことはありません。
XAMLからBinding できるのはメソッドの他、そのメソッドに対する引数(パラメータ)も同時に渡すことが出来ます。
Executeメソッドをもう一度思い出してみて下さい。
1 2 3 4 |
public void Execute(object parameter) { ・・・・・・・・ } |
object型のparameter という引数がありますよね。
例えば、次の様に CommandParameter=”値”とXAMLに記述することで、任意の値を渡す事が可能です。
1 |
<Button Command="{Binding MenuCommand}" CommandParameter="3"> |
ただし、これが出来るからと言って何でもかんでも1つのCommandに処理させるのは本末転倒ですので、ご注意ください。
まとめ
いかがでしたでしょうか。
MVVMでプログラムを作成することで、画面と処理が分離できることをイメージしていただけたかと思います。
ただ、Windows Form のイベントハンドラ形式に比べて、かなりコード量が増えてしまいます。
MVVMで恩恵を受けるのは、大人数で作業分担しながら開発するような場合であり、DIYプログラミングにおいてMVVMを採用するメリットはほとんど無い事も、お分かり頂けたのではないでしょうか。
実際の大規模開発では、もっと使いやすいようにクラス化して利用したり、Prism や Livet と呼ばれるフリーのフレームワークを使って開発します。
もし本格的にMVVMでの開発を考えるなら、これらフレームワークの導入することで効率化が図れますので、一度ご確認下さい。