前提条件
OS:Windows 11
Visual Studio:Community 2022 v17.7.4
フレームワーク:.NET6.0
言語:C#、XAML
とりあえずMVCモデルについて
MVVM(Model – View- ViewModel)について考える前に、まずは基本的なMVC(Model-View-Controller)モデルについて少し考えます。
MVCモデルとは、システム開発を行う際に、役割ごとにModel、View、Controllerの要素ごとに分けてソフトウェアを開発しようという設計モデルです。
具体的なイメージとしては、WEBシステムを思い浮かべると良いかも知れません。
※実際にはCGIなど、ViewもControllerあたりが生成したりする事もありますが…。
要素 | 実装例 | 役割 |
---|---|---|
Model | Pythonなど | サーバー上で動作するシステムのメインの処理、 ビジネスロジックを担当 |
View | HTMLなど | 各ユーザーのPC上に表示したり入力に対応する画面を提供 |
Controller | JavaScript、Perlなど | ユーザーのViewへの入力や操作に対応する処理、 ビジネスロジックへの橋渡し |
そもそも動作する環境が違ったり、開発言語が異なったりすると自然とMVCモデルに近い形にはなります。
このように各要素が独立していれば、不具合があったり、ある要素に更新が必要であったりする場合も他の要素を変更しなくても良いので、開発の生産性やメンテナンス性が向上すると言われています。
実際には、そのようにするには各要素を結ぶインターフェース、APIを当初からしっかり設計・実装しておく事が必要で、そうでないと結局システム全体を見直す必要があるという事にもなりかねませんが…。
Windowsのデスクトップアプリでは、多くの場合同一PC上で処理が完結するので、Model/View/Controllerも同じコード上で記述する事になって、ごっちゃになりがちな感じです…。
WPFアプリ開発
WPF(+.NET)での開発について見ていきます。
一つのウィンドウのファイル構成
WPF+.NETのデスクトップアプリケーションを新規作成すると、下記の様な画面になります。
View(画面)を担当するMainWindow.xamlと、Model(ロジック)を担当するMainWindow.xaml.csがペアで生成されます。
Formアプリ開発と似たようなデザイナ画面がありますが、画面として生成されるのはXAML(eXtendable Application Mark-up Language)と呼ばれるマークアップ言語になります。
C#のコードとして自動生成されるMainWindowクラスのコメント(Summary)には
「Interaction logic for MainWindow.xaml」
とあるので直訳すると「MainWindo.xamlに対する対話ロジックを実装するクラス」という様な位置づけでしょうか。
コード・ビハインド(Code-Behind)
マイクロソフト社のページによると「コード・ビハインド」とは、
コードビハインドとは、XAML ページがマークアップ コンパイルされる際に、マークアップ定義オブジェクトと結合されるコードを表すために使用される用語です。
https://learn.microsoft.com/ja-jp/dotnet/desktop/wpf/advanced/code-behind-and-xaml-in-wpf?view=netframeworkdesktop-4.8
という事なのですが、『画面を記述したXAMLのコンパイル時にそれに対応したコードが結合されますよ。そのコードの事ですよ』という事で、『XAMLがメイン、Codeがサブ』という感じでしょうか?
なので、『(XAMLの)後ろのコード』⇒『コード・ビハインド(Code-Behind)』…(?)
ModelとViewは分離されている?
View(画面)がXAMLで記述され、Model(ロジック)がC#で記述されるので、各々分離されているとも言えますが、イベントハンドラーを定義したり、C#からXAMLの要素を名前を使ってアクセスするような設計をすると、ViewとModelの結びつきが強くなりすぎます。
『結びつきが強い』と言うのは、ViewとModelがお互いの実装に依存してしまい、開発も両方同時に開発したり、修正も両方同時に行わなければならないと言うような事が発生します。
例えば下記の様なButtonを押したらTextBlockの表示内容が変わるものをイベントハンドラーを使って記述したとします。
【MainWindw.xaml】
<略> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBlock x:Name="TextBlock1" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48" Text="Hello!"/> <Button x:Name="Button1" Grid.Row="1" Margin="8" Width="100" HorizontalAlignment="Center" Content="PUSH" Click="Button1_Click"/> </Grid> <略>
【MainWindw.xaml.cs】
using System.Windows; namespace WpfAppTest1 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { // コンストラクタ public MainWindow() { InitializeComponent(); } // ボタンが押された時の処理 private void Button1_Click(object sender, RoutedEventArgs e) { TextBlock1.Text = "World!"; } } }
このようなコードの場合、XAMLの中にC#のメソッド名(イベントハンドラ名)が、C#の中にXAMLの要素名が含まれており、各々がお互いが存在する事が必要な状態になり、XAML/C#で言語レベルで分かれているけど、結局切り離せない状態になっている事になります。
この構造だと「画面とロジック開発が別々にできない」「画面を更新しようとするとロジックまで変更が必要になる」という事がデメリットとして語られる事が多いですが、個人的には画面もロジックも同時に開発する事が多いのであまり気になりません…^^;
ModelとViewを分離するViewModel
Model(ロジック)とView(画面)を分離するには、直接参照を取り除く必要があります。
ただし、直接参照を取り除いたとしても、ロジックから画面の内容の更新をしたり、画面のボタンが押された時の処理をロジックで実行したりする必要があります。
それを実現するためには下記の様にお互いを仲介する物が入る構造になるでしょう。
この仲介者がいわゆるViewModel(ビューモデル)になります。
Model(ロジック)やView(画面)はViewModelに対して値を設定したりします。ただ値が変更された事やイベントが発生した事をViewModelに設定しても、その先(相手)にはそのままでは伝わらないので、『通知』を使った仕組みが必要になります。
そして、図を見れば分かりますが、View(画面)とModel(ロジック)のお互いの参照はなくなりますが、View(画面)はViewModelを参照し、Model(ロジック)はViewModelを参照しているので、ViewModelが両者にとって必要なものになります。
※実際にはView(XAML)の記述にViewModelが必須という訳ではないですが、ViewModelを指定した方が、IDEのIntelisenseで候補を表示してくれたりするので、あるに越した事はないでしょう。
MVVMの実装例
ここからは、MVVM(Model-View-ViewModel)の実装例を示していきます。
サンプルアプリ仕様
ここでは「int型のSomeValueを表示・変更、処理する」下記の様なアプリを考えます。
項目 | 型 | 説明 |
---|---|---|
SomeValue | int | 取りうる範囲:0~1,000、初期値:100 |
TextBox1 | TextBox | SomeValueの現在の値を表示。 また、入力された値でSomeValueを更新。 ※入力された文字列が整数でなければ最小値に設定する |
Slider1 | Slider | SomeValueの現在の値を表示。 また、Sliderの位置の更新でSomeValueを更新。 |
Button1 | Button | 現在のSomeValueの値を1/2に更新 |
MVVMなしでの実装例
まずは、MVVMを使わず、イベントハンドラー等を使って実装してみます。
【MainWindow.xaml】
<Window x:Class="NoMVVMSample.MainWindow" …略… Title="MainWindow" Height="100" Width="200"> <Grid Margin="8"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Text="SomeValue:"/> <TextBox x:Name="TextBox1" Grid.Column="1" VerticalAlignment="Top" TextChanged="TextBox1_TextChanged" /> <Slider x:Name="Slider1" Grid.Row="1" TickFrequency="100" TickPlacement="BottomRight" ValueChanged="Slider1_ValueChanged"/> <Button x:Name="Botton1" Grid.Row="1" Grid.Column="1" VerticalAlignment="Top" Content="x0.5" Click="Botton1_Click"/> </Grid> </Window>
【MainWindow.xaml.cs】
public partial class MainWindow : Window { // SomeValue変数 private int SomeValue = 100; // SomeValue最小値 private const int MinSomeValue = 0; // SomeValue最大値 private const int MaxSomeValue = 1000; // コンストラクタ public MainWindow() { InitializeComponent(); // SliderのMin/Maxを設定 Slider1.Minimum = MinSomeValue; Slider1.Maximum = MaxSomeValue; Slider1.Value = SomeValue; } // SomeValueを更新する private void UpdateSomeValue(int value) { // Min/Maxで補正 if(value < MinSomeValue) { SomeValue = MinSomeValue; } else if(value > MaxSomeValue) { SomeValue = MaxSomeValue; } else { SomeValue = value; } // コントロールへ反映 TextBox1.Text = SomeValue.ToString(); Slider1.Value = SomeValue; } // TextBox1の内容が変更された時の処理 private void TextBox1_TextChanged(object sender, TextChangedEventArgs e) { int newVal = MinSomeValue; // 文字列をパース int.TryParse(TextBox1.Text, out newVal); // 更新 UpdateSomeValue(newVal); } // Slider1の値が変更された時の処理 private void Slider1_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { // 更新 UpdateSomeValue((int)Slider1.Value); } // Button1が押された時の処理 private void Botton1_Click(object sender, RoutedEventArgs e) { // 1/2へ更新 UpdateSomeValue(SomeValue / 2); } }
ビルドして実行すると、仕様通り動作します。
イベントハンドラーを使ってロジックを記述した際、問題になるのは、画面の仕様変更の時等でしょう。
「TextBoxでの変更は不要だから、TextBlockにしよう」
「TextBoxだけあれば値を指定できるからSliderはいらないや」
「Sliderで大まかな位置が指定できればいいからTextBoxはいらないや」
「Sliderでなく、ScrollBarにしてみようか」
などなど
画面のコントロールが増減したりや種類が変わったりすれば、コントロール名やイベントハンドラ名も変更になり、ロジック側のコードも修正を余儀なくされます…。
MVVMでの実装例
次にMVVMを使って(ViewModelありで)実装してみます。
ViewModelの実装方法はネットには色々転がっていて、どれが正解と言うものはないと思いますが、自分は下記のページを参考にしています。
コマンド実行
https://learn.microsoft.com/ja-jp/dotnet/maui/fundamentals/data-binding/commanding
※Microsoft社のサンプルコードは不完全な部分が多いですが…。
ViewModelの仕様
ViewModelを実装する前にどのようなプロパティが必要か、コマンドが必要かをまとめてみます。
種類 | プロパティ/ コマンド名 | 型 | 補足 |
---|---|---|---|
プロパティ | Value | int | ロジックのSomeValueに相当。 Valueが変更された際は、Textもそれに応じて変更 |
プロパティ | MinValue | int | Valueが取りうる最小値 |
プロパティ | MaxValue | int | Valueが取りうる最大値 |
プロパティ | Text | string | Textが変更された際は、Valueもそれに応じて変更 |
コマンド | HalfCommand | Command | ViewModel内では処理はしないで、 イベントハンドラを発火させるのみとする |
ネットのサンプルを見ていると、ViewModelの中にロジックを記述するような物もありますが、それをすると、ロジック変更時にViewModelを変更する必要があり、そうなるとViewModelを参照しているView(画面)まで影響する可能性が出てくるので、個人的にはViewModelの中にはロジックは極力書かない方がいいのかと思います。
ViewModelの実装
ViewModelの仕様に基づいてViewModelを下記の様に実装しました。
(詳細な説明は割愛します。コメントを参考にしていただければと思います)
【MyViewModel.cs】
using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace MVVMSample1 { // ViewModel実装例 class MyViewModel : INotifyPropertyChanged { // プロパティ変更イベントハンドラー public event PropertyChangedEventHandler? PropertyChanged; // コマンドイベントハンドラー public event CommandEventHandler? CommandExecuted; // 値を1/2にしたい時のコマンド public ICommand HalfCommand { private get; set; } // コンストラクタ public MyViewModel() { // 値を1/2する際のコマンド HalfCommand = new Command( execute: () => { OnCommandExecuted(nameof(HalfCommand)); }, canExecute: () => { return true; }); } // 値 private int _value = 0; public int Value { get { return _value; } set { // プロパティの更新 if (SetProperty(ref _value, value) == true) { // Textも更新 Text = Value.ToString(); } } } // Valueの取りうる最小値 private int _minValue = 0; public int MinValue { get { return _minValue; } set { // プロパティの更新 SetProperty(ref _minValue, value); } } // Valueの取りうる最大値 private int _maxValue = 100; public int MaxValue { get { return _maxValue; } set { // プロパティの更新 SetProperty(ref _maxValue, value); } } // テキスト(Valueの文字列) private string _text = string.Empty; public string Text { get { return _value.ToString(); } set { int val = MinValue; // 整数としてパース if (int.TryParse(value, out val)) { // 範囲補正 if (val < MinValue) { val = MinValue; } else if (_maxValue < val) { val = MaxValue; } } else { // 整数以外が入力された val = MinValue; } // プロパティの更新 if (SetProperty(ref _text, val.ToString()) == true) { // Valueも更新 Value = val; } } } // プロパティ更新処理 bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Object.Equals(storage, value)) return false; storage = value; // プロパティ変更イベント OnPropertyChanged(propertyName); return true; } // プロパティ変更イベントを発火 protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // コマンドイベントを発火 protected void OnCommandExecuted([CallerMemberName] string propertyName = null) { CommandExecuted?.Invoke(this, new CommandEventArgs(propertyName)); } } // コマンドイベントハンドラデリゲート宣言 public delegate void CommandEventHandler(object sender, CommandEventArgs e); }
ViewModelクラスはプロパティの値を変更して、それを通知する事で画面やロジックへ通知できる様にするため、INotifyPropertyChangedインターフェースを実装する必要があります。
ネットでは、プロパティ名を文字列として引数に渡すサンプルを良く見ますが、文字列や数値を直にコーディングするのは、個人的には好きではありません…。文字列や数値は単なるデータのため、時間が経って後から見たり、他人が見ると意味が分からなかったり、コード中の文字列や数値は打ち間違えてもエラーにならないので、コードの正しさを確保するのが難しいからです。
また、プロパティのSetで(更新時)元の値と違うかどうかはSetPropertyメソッド内でチェックしています。もし、プロパティの設定以外の処理をするなら、SetPropertyメソッドの戻り値がtrueの時のみ実施する様にします。そうしないと設定がループする可能性があるためです…。
今回の例ではコマンドは一つですが、コマンドが複数存在する時にコマンドごとにクラスを実装するのも大変なので、前述のMS社のページを参照にベースとなるコマンドクラスを作成します。(と言ってもMS社のページにはコードはありませんでしたが…)
【Command.cs】
// コマンドクラス public class Command : ICommand { // 実行時処理 private Action execute; // 実行可能かを取得 private Func<bool> canExecute; // コンストラクタ public Command(Action execute, Func<bool> canExecute) { this.execute = execute; this.canExecute = canExecute; } // 実行可能化が変更になった際の処理 public event EventHandler? CanExecuteChanged; // 実行可能かを取得 public bool CanExecute(object? parameter) { return canExecute(); } // コマンドの実行 public void Execute(object? parameter) { execute(); } }
次にコマンドイベントハンドラに渡す、コマンドイベント引数を作成します。(といってもコマンド名を持たすだけですが…。)
【CommandEventArgs.cs】
// コマンドイベント引数クラス(共通) public class CommandEventArgs : EventArgs { // コマンド名 public string CommandName { private set; get; } // コンストラクタ public CommandEventArgs(string commandName) { CommandName = commandName; } }
画面のXAMLは下記の様になります。
【MainWindow.xaml】
<Window x:Class="MVVMSample1.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:MVVMSample1" mc:Ignorable="d" Title="MainWindow" Height="100" Width="200"> <Window.DataContext > <local:MyViewModel/> </Window.DataContext> <Grid Margin="8"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBlock Text="SomeValue:"/> <TextBox Grid.Column="1" VerticalAlignment="Top" Text="{Binding Text}"/> <Slider Grid.Row="1" TickFrequency="100" TickPlacement="BottomRight" Minimum="{Binding MinValue}" Maximum="{Binding MaxValue}" Value="{Binding Value}"/> <Button Grid.Row="1" Grid.Column="1" VerticalAlignment="Top" Content="x0.5" Command="{Binding HalfCommand}"/> </Grid> </Window>
ポイントとしては以下になります。
- Windows.DataContextでMyViewModelを設定
- TextBox1のText、Slider1のMinimum, Maxmum, ValueはBindingを利用してViewModelから取得
- ButtonのCommand(Click時の動作)にBindingを利用してViewModelで処理
- コントロールの名前は指定しなくても良くなる
ViewModelを利用すると、コントロールの名前を指定しなくても良くなりますが(ロジックが名前を指定して処理する事がなくなるため)、コントロールが増えると名前が無いとどれがどれだか分からなくなったり、設計や試験でコントロール名がないと困る事になったりするので、個人的には必ず名前(x:Name)は付ける事にしています。
ちなみに、ロジック(MainWindow.xaml.cs)を実装していなくても、この時点で一部動作させる事は可能です。(当然仕様通りには動きませんが…)
ロジックのC#コードは下記の様になります。
【MainWindow.xaml.cs】
/// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { // SomeValue最小値 private const int MinSomeValue = 0; // SomeValue最大値 private const int MaxSomeValue = 1000; // SomeValueデフォルト値 private const int DefaultSomeValue = 100; // SomeValue変数 private int SomeValue = DefaultSomeValue; // コンストラクタ public MainWindow() { InitializeComponent(); // ビューモデルのインスタンスを生成 MyViewModel myViewModel = new MyViewModel(); // 初期値の設定 myViewModel.MinValue = MinSomeValue; myViewModel.MaxValue = MaxSomeValue; myViewModel.Value = SomeValue; // イベントハンドラの設定 myViewModel.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(MyViewModel.Value)) { // 値を更新 SomeValue = myViewModel.Value; } }; // コマンドハンドラの設定 myViewModel.CommandExecuted += (s, e) => { if(e.CommandName == nameof(MyViewModel.HalfCommand)) { // 値を半分にする myViewModel.Value = SomeValue / 2; } }; // データコンテキストへ設定 this.DataContext = myViewModel; } }
これをビルドして実行すると仕様通りに動作します。
注:上記の実装では、TextBoxの値の変更はTextBoxのFocusLost時しか発生しない(ViewModelのTextのSetが呼ばれない)ので、TextBoxにFocusがある状態で文字を追加しても、ENTERキーを押しても値は更新されません(TABキーを押すなどして別のコントロールへFocus移動させるなどする必要があります)。TextBoxの挙動についてはもう少し工夫する必要があるでしょう…。
この状態でXAMLの適当なコントロール(もしくはすべてのコントロール)を削除しても、ビルドは通り、アプリも問題なく実行できます。
MVVMについて思う事
Model-View-ViewModelの構成は下記の様になっていました。
個人的な感想
個人的には「今回例に挙げたようなアプリなら、MVVMを使わない方がコードが少なく済むからいいな…」、「変更したいプロパティをすべてViewModelに実装しないといけないのか…。イベントハンドラを使ったり、コントロールを直接触れば任意のプロパティを操作できるのになぁ」と思ってしまいます。
ただ、確かにView(画面)とModel(ロジック)はきれいに分かれるのは設計上きれいですし、コードもシンプルになります(ViewModel以外は…、と言ってもViewModelは書き方が決まっているのでプロパティやコマンドの量だけの問題ですが…)。
また、MVVMでない場合、アプリ規模が大きくなってくると、ロジックのコードが、イベントハンドラだらけ、コントロールの制御コードだらけになって、画面修正が必要になった時に大変なのも確かです。
ViewModelの設計が重要
MVVMでアプリを作成する際に、ViewModelの設計をしっかりしないといけないのも確かです。
ViewModelの仕様が頻繁に変わるようになると、View(画面)もModel(ロジック)両方に(結局全体)に手を入れなくてはならなくなります。
手段が目的化しない様に…
もともと「MVVMモデルを用いる事で、メンテナンス性の良いアプリ(コード)を開発するんだ」という目的で始めたものが、いつの間にか「アプリはMVVMで開発するものだ」と手段が目的化してしまうと、「MVVMの形式になってさえいれば」という考えでアプリ開発が進み、もともとの目的が忘れ去られ、結局品質が下がってしまう可能性があります。
「ModelにイベントハンドラやViewへの直接参照でコードが肥大化」してたものが「ViewModelにロジック詰め込み過ぎて、Modelで肥大化してたコードがViewModelに移っただけ…」という事にもなりかねません…。
個人的には、開発手法・設計パターンより、第一に「しっかりとした要件定義」、「しっかりとした設計」が重要だと考えています。(それが一番難しいのですが…)
混じっても良いんでは…?
個人的には、一部はMVVMを使って、一部はイベントハンドラー等を使うといった良いとこ取りで開発しても良いのではと思います。その方がアプリ開発も速い気がします。
ただ、理解不足のまま、両方を混ぜると思った様に動かなくなる可能性もあるので、「良く分からないから」という理由で混ぜるのは危険とも思います。
「どうしても将来的にView(画面)の更新は必須」という事であれば、MVVMで開発する必要があるかも知れません。(予算・納期の関係もありますが…)
武器にはなる
個人的には「MVVMが正義。そうでないものは悪。」みたいな考えはありません。
MVVMはプログラムの開発手法の一つだと考えています。
だからと言って、知らなくて良いというものではなく、MVVM(少なくともとっかかり)を理解してかないと、MVVMに基づいて記述されたコードを理解する事が出来ません。
また「MVVMで開発してほしい」と依頼があっても、断らざるを得なくなります。
MVVMに限った事でないですが、「知っている事」「利用できる事」は武器・力になるので、身に着ける事は損はないでしょう。
ちょっと気になる事
ネットの記事・投稿を見ていると(日本語・英語関わらず)、
「これをMVVMで書けばこうなる。コード・ビハインドで書けばこうなる」
と言うような記述を良く見かけます。
前述の通り「コード・ビハインド」とはXAMLに結合されるコードを指す事なので、「コード・ビハインド」が「MVVC」の対義語の様に利用されているのは違和感を感じます。
「これをMVVMを使って書けばこうなる。MVVMを使わずに書けばこうなる」
と言う言い回しが正しいのでは…?と思ってしまいますが、まぁ、良いか…。