.NET用 MVVM Toolkit v8でMVVMコードを短く

[以前紹介](https://qiita.com/hqf00342/items/40a753edd8e37286f996)したMVVMライブラリ [CommunityToolkit.Mvvm](https://github.com/CommunityToolkit/dotnet) のバージョンが ver8 になりそうなのでpreviewをご紹介。  

ver7の軽さはそのまま。ver8 の目玉はMVVMコードの記述簡易化です。  

https://devblogs.microsoft.com/ifdef-windows/announcing-net-community-toolkit-v8-0-0-preview-3/

ver7の紹介記事はこちら
https://qiita.com/hqf00342/items/40a753edd8e37286f996

**2024年12月15日追記**
ver8.4で、C#13の[部分プロパティを利用した変更通知プロパティ](https://qiita.com/hqf00342/items/d12bb669d1ac6fed6ab6#%E5%A4%89%E6%9B%B4%E9%80%9A%E7%9F%A5%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)に対応したため記事に反映しています。

https://devblogs.microsoft.com/dotnet/announcing-the-dotnet-community-toolkit-840/

## 新しいViewModelの記述方法

早速ですが新旧の書き方比較をしてみます。
最初に旧来(ver7)の方法で、変更通知できる `Name` プロパティ と `GreetCommand` コマンドを書いてみます。

```csharp:Vm.cs (ver7) 
internal class Vm : ObservableObject
{
    private string _name;
    public string Name 
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }

    private IRelayCommand<string> greetCommand;

    public IRelayCommand<string> GreetCommand => greetCommand ??= new RelayCommand<string>(Greet);

    private void Greet(string user)
    {
        Debug.WriteLine($"Hello {user}!");
    }
}
```

これをver8で書くと以下のコードになります。

```csharp:Vm.cs (ver8) 
internal partial class Vm : ObservableObject
{
    [ObservableProperty]
    private string name;

    [RelayCommand]
    private void Greet(string user)
    {
        Debug.WriteLine($"Hello {user}!");
    }
}
```

ずいぶんと短くなりました。作ったプロパティやコマンドすら見当たらないですが、1つずつ説明していきます。

## コードを簡潔に

C#の進化とともにMVVMのコード記述量は減りましたがそれでもまだ面倒でスニペットや便利なライブラリが良く使われます。海外では [ReactiveUI](https://www.reactiveui.net/)、日本では [ReactiveProperty](https://github.com/runceel/ReactiveProperty)あたりでしょうか。 [Fody](https://github.com/Fody/Fody) のようにILを直接書き換えるのも面白い試みです。
（興味ある方は @soi さんの [INotifyPropertyChangedプロパティ実装方法まとめ C#3からC#7、Fodyも](https://qiita.com/soi/items/d0c83a0cc3a4b23237ef) をご覧ください）

今回のMVVM Toolkit ver8では roslyn のソースジェネレータで解決してます。  
面倒なコードはコンパイラが生成しているのでライブラリにありがちなオーバヘッドもありません。

## ViewModelクラス定義

ver7までは `ObservableObject` (他ライブラリでいうBindableBase) を継承していました。

``` csharp
internal class Vm : ObservableObject
{
}
```

上の書き方でもOKですが、ver8から`INotifyPropertyChanged`属性で作れるようになります。またソースジェネレータのために `partial` とします。

``` csharp
[INotifyPropertyChanged]
internal partial class Vm
{
}

//多重継承の必要がなければこちらが推奨
internal partial class Vm : ObservableObject
{
}
```

C# は多重継承ができないため、データバインディング用に基底クラスを使うとほかのクラスを継承できなかった問題が解決します。

追記：アナライザの警告で ObservableObject を継承するパターンを推奨していますので別クラスの継承が必要なければ `partial` を付けて継承パターンを使いましょう。


## ICommandの記述

ICommandはこれまで以下のように実装していました。

``` csharp
private IRelayCommand<string> greetCommand;

public IRelayCommand<string> GreetCommand => greetCommand ??= new RelayCommand<string>(Greet);

private void Greet(string user)
{
    Debug.WriteLine($"Hello {user}!");
}
```
ver8からは`[RelayCommand]`属性を付けることで `～Command` が自動生成されます。（previewでは「ICommand」属性でしたがver8.0正式版で「RelayCommand」属性に変更）

``` csharp
[RelayCommand]
private void Greet(string user)
{
    Debug.WriteLine($"Hello {user}!");
}
```
XAMLからの使い方は変わりません。

``` xml:xaml
<Button Content="Greet" Command="{Binding GreetCommand}" CommandParameter="test" />
```

## 変更通知プロパティ

`INotifyPropertyChanged` 実装プロパティはいままでは以下のように記述していました。C#の進化とライブラリによってだいぶ短く書けるようになりました。
```csharp
private string _name;
public string Name 
{
    get => _name;
    set => SetProperty(ref _name, value);
}
```

ver8からは以下のように書けます。

```csharp
// ver8.0
// Name プロパティが自動生成される。
[ObservableProperty]
private string name;
```

さらにver8.4で[部分プロパティに対応](https://qiita.com/hqf00342/items/cb97849981441e2020cf)しました！
```cs
//ver8.4以降はプロパティを書けます。partial が必要。
[ObservableProperty]
public partial string Name { get; set; }
```
ver8.0はバッキングフィールド `name`を書いていましたが、ver8.4以降は`Name`プロパティを記述できるようになりました。ただしC#13時点(.NET9/2024年)では以下の注意点が必要です。
- プロパティに `partial` キーワードが必要です。
- C#13に field キーワードが盛り込まれなかったため、csprojファイルに`<LangVersion>preview</LangVersion>`が必要です。


XAMLでの使い方は変わりません。

``` xml:xaml
<TextBlock Text="{Binding Name}" />
```

また、変更通知プロパティで変更前と変更後に処理を挟みたい時があります。`Onプロパティ名Changing()`、`Onプロパティ名Changed()` というメソッドを用意しておくと勝手に呼び出されるようになります。（詳しくは[preview3の紹介記事](https://devblogs.microsoft.com/ifdef-windows/announcing-net-community-toolkit-v8-0-0-preview-3/)を参照)


```csharp
[ObservableProperty]
private string name;

partial void OnNameChanging(string oldValue, string newValue)
    =>Console.WriteLine($"Nameプロパティが {newValue} に変更されようとしてます");

partial void OnNameChanged(string oldValue, string newValue)
    =>Console.WriteLine($"Nameプロパティが {newValue} に変更されました");
```


## おわりに

以下ver8.0時点の感想はver8.4で改善されました。継続的に改善されるライブラリはいいですね。

>(ver8.0時点の感想)
> MVVMのコード記述が減るのは大歓迎ですが、この書き方で良いのかは疑問が残ります。僕らが望んでいたのは表(プロパティ等)を書いたら裏(backing-field等)を自動生成してくれる、だったのですが逆になってます。確かにソースジェネレータならこうなるよな、とは思うのですが・・。コード書く人はVisualStudioのインテリセンスで何とかなりますが、コードを読む人は自動生成された表側を推測しないといけないのが難点です。~~

→2年越しにver8.4で部分プロパティに対応したため、読みやすさも改善したと思われます。C#13に未導入のfield機能を使わなくても実装出来たろうに、と思うことはありますが。

ver8 の自動生成機能はソースジェネレータを利用しているため roslyn 4.x(VisualStudioならば2022)以降が必要になります。環境が用意できない場合でもソースジェネレータ以外の機能は使えます。
Messenger、コレクション系の通知機能も増えていますが正式リリース時に気が向いたら書きたいと思います。

### 参考

- [preview1の紹介文](https://devblogs.microsoft.com/ifdef-windows/announcing-net-community-toolkit-v8-0-0-preview-1/)
- [preview3の紹介文](https://devblogs.microsoft.com/ifdef-windows/announcing-net-community-toolkit-v8-0-0-preview-3/)
- https://github.com/CommunityToolkit/dotnet
- [Announcing .NET Community Toolkit 8.4! Partial properties support for MVVM, new analyzers, and more!](https://devblogs.microsoft.com/dotnet/announcing-the-dotnet-community-toolkit-840/)