MVVMに則るWPFプログラムを設計する際のポイントを自分なりに整理してみる

WindowsのGUIをWPFで開発する際の王道について再入門しつつ書き記す（適宜修正。自分のための備忘録）。ここでは、画面コンポーネントとは「MVVMパターンに則り構築されたコンポーネント」と定義する。

### 残件 ～自分の理解を見直す～
* ModelからViewに向かって更新がかかるケースで適宜

# アーキテクチャ

## MVVMパターン

### 概念レベルのMVVM

マイクロソフトが示す絵は、MVVMの登場者間の関係、すなわち構造面のアーキテクチャを次のようにポンチ絵で示している。

[![マイクロソフトから引用](https://learn.microsoft.com/ja-jp/dotnet/architecture/maui/media/mvvm-pattern.png)](https://learn.microsoft.com/ja-jp/dotnet/architecture/maui/media/mvvm-pattern.png)


## 画面コンポーネントのためのアーキテクチャ

### 構造面のアーキテクチャ

コンセプトをプログラミングに活かすため、自分なりに構造面のアーキテクチャを少し具体化したものは次の通り。

```plantuml
@startuml

class 人 << (E,gray) 外部 >>
class 外部 << (E,gray) 通信やデータベース等 >>
interface Window << (I,gray) .Net Framework >>
class XXXWindow << xaml.cs >> {
}
note right of XXXWindow
    View。代表的なクラスはMainWindow
end note

class Model {
    + 画面で使用する形式のデータ
}

class XXXViewModel {

}

interface ICommand << (I,gray) .Net Framework >> {
    + CanExecuteChanged
    + Execute()
}
class XXXCommand
ICommand <|.right. XXXCommand

interface INotifyPropertyChanged << (I,gray) .Net Framework >>
XXXViewModel .left.|> INotifyPropertyChanged

人 --> XXXWindow : 操作を受け入れる
人 <.. XXXWindow : 見る
Model -up-o XXXViewModel
XXXViewModel o-- XXXCommand
XXXViewModel .up.> XXXWindow : 更新通知(RaisePropertyChanged)
Window <|.right. XXXWindow
XXXWindow o-- "DataContext" XXXViewModel
Model ..> XXXViewModel : 更新通知
Model --> 外部
XXXCommand --> 外部

@enduml
```

### 振る舞い面のアーキテクチャ

登場者間の役割分担を示すため、振る舞い面のアーキテクチャをシーケンス図にしてみた。

```plantuml
@startuml

actor user
boundary View
control ViewModel
control DelegateCommand << ICommand >>
entity Model

user -> View : クリック
View -> ViewModel : Data Binding
ViewModel -> ViewModel : プロパティ更新
ViewModel -> Model : 必要に応じて更新
user -> View : クリック
View -> ViewModel : 処理
ViewModel -> DelegateCommand : （委譲登録されている）処理(引数はViewModel内の情報)
DelegateCommand -> DelegateCommand : 何か処理
DelegateCommand -> Model : 必要に応じて更新

@enduml
```

```plantuml
@startuml

actor user
boundary View
control ViewModel
entity Model
Model -> ViewModel : 更新イベント（WPF縛り無）
ViewModel -> View : RaisePropertyChanged
View -> user : 表示

@enduml
```

### 言葉で補完してみる

検算として、画面コンポーネントにかかわる最も重要な責務分担ポイントは次の通り。

  * View
    * GUI（人とシステムのインタフェース、バウンダリ）
    * 画面デザイン（入力方法、表示方法）
  * Model
    * システムの根幹又は根幹に対するプロキシの役目
      * 画面一つ程度で済むような小さいプログラムの場合は、Model＝システムの根幹
      * 複数計算機からなるような大きいシステムの場合は、Model＝システムの根幹に対するプロキシ
  * Command
    * システムの中で処理又は処理の入り口の役目
    * WPF固有の都合により登場
      * WPFはViewとViewModelとの関係においてデータバインドとコマンドバインド（造語）があり、コマンドバインドのために登場
  * ViewModel
    * Viewと、Model又はCommandとの橋渡し役
    * 画面内の閉じる情報を保持（≒プロパティ）

# 自分なりの精査

自分なりの理解が正しいか、具体的なプログラム、システムを設計してみることで精査する（見直す）。

## BMI計算機

### 要求仕様

次のような画面プログラムを設計してみる。

```plantuml
@startsalt
{
  年齢 |
  {+
      57
  } 歳
  体重 |
  {+
    76.8
  } |
  ^kg^
  身長 |
  {+
    176.2
  } |
  ^cm^
  [BMI計算]
  BMI |
  {+
    24.74
  }
  判定結果 |
  {+
    適正
  }
}

@endsalt
```

### 構造面の設計

```plantuml
@startuml

class ユーザ << (E,gray) 外部 >>
class 画面 << View >>
class BMI計算機 << Model >> {
  + BMIを算出する(身長, 体重) : BMI
  + 判定する(BMI, 年齢) : 判定結果
}
class BMI算出コマンド << Command >> 
class 画面とBMI計算機の橋渡し << ViewModel >> {
  + 身長 : double
  + 体重 : double
  + BMI : double
  + 判定結果 : string
}
ユーザ -right-> 画面
画面とBMI計算機の橋渡し o-- BMI計算機
画面とBMI計算機の橋渡し o-- BMI算出コマンド
画面 -- 画面とBMI計算機の橋渡し
BMI算出コマンド -right-> BMI計算機

@enduml
```

### 振る舞い面の設計

```plantuml
@startuml

actor ユーザ
boundary 画面
control ViewModel
control BMI算出コマンド
entity BMI計算機

ユーザ -> 画面 : 身長を入力
画面 -> ViewModel : 身長を更新
ユーザ -> 画面 : 体重を入力
画面 -> ViewModel : 体重を更新
ユーザ -> 画面 : BMI計算ボタンを押す
画面 -> ViewModel : 実行
ViewModel -> BMI算出コマンド : (委譲登録されているコマンド)実行(身長, 体重)
BMI算出コマンド -> BMI計算機 : BMIを算出する

@enduml
```

# 参考

* [「MVVMモデルの簡単な解説」@tkhr68000さん](https://qiita.com/tkhr68000/items/aaa11199bec0fb30ae01)
* [@IT 連載：WPF入門](https://atmarkit.itmedia.co.jp/ait/subtop/features/dotnet/app/introwpf_index.html)
* [連載：WPF入門 第6回　「コマンド」と「MVVMパターン」を理解する](https://atmarkit.itmedia.co.jp/ait/articles/1011/09/news102_3.html)

# その他覚書

* Modelは必ずしも必要というわけではない。小さいModelや肥大化しないModelの場合は、ViewModel内にModelを内包する方が簡素で見やすいコードになることもある。
* ViewModelでバインドされているプロパティの変更通知は大きく3通りの書き方がある。
  * プロパティ名を指定する方法
```csharp:
// プロパティ名を指定する方法
RaisePropertyChanged(nameof(プロパティ名));

protected void RaisePropertyChanged(string propertyName)
{
    var h = PropertyChanged;
    if (h != null)
    {
        h(this, new PropertyChangedEventArgs(propertyName));
    }
}
```
  * CallerMemberName属性を使ってプロパティ名を省略する方法
```csharp:
RaisePropertyChanged();

protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
```
  * プロパティ名を文字列で指定する方法
```csharp:
if (PropertyChanged != null)
{
    PropertyChanged(this, new PropertyChangedEventArgs("プロパティ名"));
}
```
* ICommandの解説
  * CanExcecuteChanged
    コマンドが実行可能かどうか示す状態に変化があったことを通知するイベント
  * CanExecute
    コマンドが実行可能かどうか返すメソッド
  * Execute
    コマンドの処理を実行するメソッド