前回のアプリケーションはシンプルすぎてModelがありませんでしたが、今度はシンプルな四則演算アプリケーションでModelまで含んだコード例を示したいと思います。MVVMの基本クラスは、Prism.Mvvmのクラスを使用します。
Modelの作成
ModelはなるべくプレーンなC#のクラスになるように心がけます。そして状態の変更をINotifyPropertyChangedを通じて外部に通知します。今回は、左辺値、右辺値、計算方法、計算結果をステートとして持たせます。INotifyPropertyChangedを実装したクラスなので、PrismのBindableBaseクラスを基本クラスとして使用します。 左辺値、右辺値、計算結果はdouble型で保持して、計算結果はOperationTypeというenum型を定義してそれを使用しています。コードを以下に示します。
public class Calc : BindableBase { private double lhs; public double Lhs { get { return this.lhs; } set { this.SetProperty(ref this.lhs, value); } } private double rhs; public double Rhs { get { return this.rhs; } set { this.SetProperty(ref this.rhs, value); } } private OperatorType operatorType; public OperatorType OperatorType { get { return this.operatorType; } set { this.SetProperty(ref this.operatorType, value); } } private double answer; public double Answer { get { return this.answer; } set { this.SetProperty(ref this.answer, value); } } } public enum OperatorType { Add, Sub, Mul, Div, }
次に、アプリケーション全体を示すクラスを作成します。趣味の問題ですが、私は、このクラスから各Modelのクラスへたどれるように作っています。クラス名はAppContextという名前で作成しました。このクラスでは、アプリケーション全体でグローバルに持たせる状態を定義しています。今回は、アプリケーションのメッセージを表示させるようにしています。そして、先ほど定義したCalcクラスもプロパティとして定義します。Calcクラスから必要に応じてメッセージが設定できるようにCalcクラスに自分自身を渡しています。
public class AppContext : BindableBase { private string message; public string Message { get { return this.message; } set { this.SetProperty(ref this.message, value); } } public Calc Calc { get; private set; } public AppContext() { this.Calc = new Calc(this); } }
Calcクラス側には、AppContextクラスを受け取るコンストラクタとフィールドを定義します。
private AppContext appContext; public Calc(AppContext appContext) { this.appContext = appContext; }
そして、Calcクラスに計算ロジックを記述します。計算ロジックは0除算のときにエラーメッセージを出す以外は直に計算するだけにしました。
public void Execute() { switch (this.OperatorType) { case OperatorType.Add: this.Answer = this.Lhs + this.Rhs; break; case OperatorType.Sub: this.Answer = this.Lhs - this.Rhs; break; case OperatorType.Mul: this.Answer = this.Lhs * this.Rhs; break; case OperatorType.Div: if (this.Rhs == 0) { this.appContext.Message = "0除算エラー"; return; } this.Answer = this.Lhs / this.Rhs; break; default: throw new InvalidOperationException(); } }
ViewModelの作成
Modelが完成したのでViewModelを作成します。ViewModelでは、まず計算方法のOperationTypeを文字列と非もづけるためのOperationTypeViewModelクラスを作成します。
public class OperatorTypeViewModel { public OperatorType OperatorType { get; private set; } public string Label { get; private set; } public OperatorTypeViewModel(string label, OperatorType operatorType) { this.Label = label; this.OperatorType = operatorType; } public static OperatorTypeViewModel[] OperatorTypes = new[] { new OperatorTypeViewModel("足し算", OperatorType.Add), new OperatorTypeViewModel("引き算", OperatorType.Sub), new OperatorTypeViewModel("掛け算", OperatorType.Mul), new OperatorTypeViewModel("割り算", OperatorType.Div), }; }
計算方法のViewModelができたので、次は、MainWindow用のViewModelを作成します。クラス名はMainWindowViewModelにしました。MainWindowViewModelクラスには、左辺値、右辺値を受け取るstring型のプロパティを定義します。ここに入力値を受け取って、計算処理のときにdouble型に変換してModelの左辺値と右辺値に設定します。そして、計算結果を格納するためのAnswerプロパティを定義します。これは、Modelから正しい値が来ることが期待できるので、素直にdouble型として定義します。
左辺値と右辺値は、あとで定義する計算をするためのDelegateCommand型のExecuteCommandプロパティに対して呼び出し可能かどうかが変更されたというイベントを発生させるためにRaiseCanExecuteChangedメソッドを呼び出しています。
最後に、画面に表示するメッセージを表示するプロパティも定義しています。
public class MainWindowViewModel : BindableBase { private string lhs; public string Lhs { get { return this.lhs; } set { this.SetProperty(ref this.lhs, value); this.ExecuteCommand.RaiseCanExecuteChanged(); } } private string rhs; public string Rhs { get { return this.rhs; } set { this.SetProperty(ref this.rhs, value); this.ExecuteCommand.RaiseCanExecuteChanged(); } } private double answer; public double Answer { get { return this.answer; } set { this.SetProperty(ref this.answer, value); } } private string message; public string Message { get { return this.message; } set { this.SetProperty(ref this.message, value); } } }
次に、計算方法のプロパティを定義します。これは先ほど作成したOperationTypeViewModel型の配列と、実際に選択されたOperationTypeViewModel型のインスタンスを格納するプロパティを定義します。計算方式のプロパティは、コンストラクタで初期化を行います。
また、現在選択されたOperationTypeViewModel型を現すプロパティでは、変更されたときに、ExecuteCommandプロパティのRaiseCanExecuteChangedメソッドを呼び出して、コマンドが実行可能かどうかに変化があったことを伝えています。
public OperatorTypeViewModel[] OperatorTypes { get; private set; } private OperatorTypeViewModel selectedOperatorType; public OperatorTypeViewModel SelectedOperatorType { get { return this.selectedOperatorType; } set { this.SetProperty(ref this.selectedOperatorType, value); this.ExecuteCommand.RaiseCanExecuteChanged(); } } public MainWindowViewModel() { this.OperatorTypes = OperatorTypeViewModel.OperatorTypes; }
次に、ModelをViewModelと接続します。今回は1画面のアプリなので、MainWindowViewModel内にModelのルートであるAppContextクラスのインスタンスを持たせる形にしました。複数画面のアプリなどで複数のViewModelからAppContextを参照するようなケースではAppContextクラスのインスタンスをもう少しグローバルにアクセス可能な形で定義するとよいと思います。(例としてAppクラスとか)AppContextクラスをフィールドとして定義したら、コンストラクタでPropertyChangedを監視して、必要に応じてModelの変更をViewModelに反映するコードを書きます。ここでは、Modelのメッセージと計算結果を監視するコードを追加します。
private AppContext appContext = new AppContext(); public MainWindowViewModel() { this.OperatorTypes = OperatorTypeViewModel.OperatorTypes; // Modelの監視 this.appContext.PropertyChanged += this.AppContextPropertyChanged; this.appContext.Calc.PropertyChanged += this.CalcPropertyChanged; } private void CalcPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Answer") { this.Answer = this.appContext.Calc.Answer; } } private void AppContextPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "Message") { this.Message = this.appContext.Message; } }
今回のようなModelとViewModelが1対1の関係にあるアプリでは問題になりませんが、ModelとViewModelが1対Nの関係にあるようなケースの場合には、ModelのPropertyChangedイベントの購読をWindowがとじたタイミングなどで解除する必要がある点に注意してください。そうしないと、ViewModelのインスタンスがいつまでだってもGCの回収対象にならないという問題があります。
最後に計算を行うCommandを定義します。ExecuteCommandという名前でDelegateCommand型のプロパティを定義してコンストラクタで初期化します。DelegateCommandのExecuteの処理では、Modelの状態をViewModelの状態をもとに最新化して、計算処理を呼び出しています。CanExecuteの処理では、入力に応じてCommandが実行可能かどうかを返しています。
public DelegateCommand ExecuteCommand { get; private set; } public MainWindowViewModel() { this.OperatorTypes = OperatorTypeViewModel.OperatorTypes; this.ExecuteCommand = new DelegateCommand(this.Execute, this.CanExecute); // Modelの監視 this.appContext.PropertyChanged += this.AppContextPropertyChanged; this.appContext.Calc.PropertyChanged += this.CalcPropertyChanged; } private void Execute() { this.appContext.Calc.Lhs = double.Parse(this.Lhs); this.appContext.Calc.Rhs = double.Parse(this.Rhs); this.appContext.Calc.OperatorType = this.SelectedOperatorType.OperatorType; this.appContext.Calc.Execute(); } private bool CanExecute() { double dummy; if (!double.TryParse(this.Lhs, out dummy)) { return false; } if (!double.TryParse(this.Rhs, out dummy)) { return false; } if (this.SelectedOperatorType == null) { return false; } return true; }
Viewの作成
最後にViewModelとViewを接続します。ViewはシンプルにViewModelに対応した入力項目と出力項目とボタンを持つだけの画面です。見た目は以下のようになります。
XAMLを以下に示します。
<Window x:Class="MVVMSample02.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:l="clr-namespace:MVVMSample02" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <l:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="5"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="25*"/> <RowDefinition Height="37*"/> </Grid.RowDefinitions> <Label Content="左辺値"/> <Label Content="計算方法" Grid.Row="1"/> <Label Content="右辺値" Grid.Row="2"/> <TextBox Grid.Column="2" TextWrapping="Wrap" Text="{Binding Lhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <ComboBox Grid.Column="2" Grid.Row="1" ItemsSource="{Binding OperatorTypes}" SelectedItem="{Binding SelectedOperatorType}" DisplayMemberPath="Label"/> <TextBox Grid.Column="2" Grid.Row="2" TextWrapping="Wrap" Text="{Binding Rhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <Label Content="答え" Grid.Row="4"/> <TextBlock Grid.Column="2" Grid.Row="4" TextWrapping="Wrap" Text="{Binding Answer}"/> <TextBlock Grid.ColumnSpan="3" Grid.Row="5" TextWrapping="Wrap" Text="{Binding Message}"/> <Button Grid.ColumnSpan="3" Content="計算" Grid.Row="3" Command="{Binding ExecuteCommand, Mode=OneWay}"/> </Grid> </Window>
実行して動作確認
実行すると以下のような画面が立ち上がります。
左辺値と計算方法と右辺値を適当に入力して計算ボタンを押すと以下のように答えに計算結果が表示されます。
0除算をしようとすると以下のようにメッセージが表示されます。
過去記事
- 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
- WPF4.5入門 その60「データバインディングを前提としたプログラミングモデル」 - かずきのBlog@hatena