以前紹介したMVVMライブラリ CommunityToolkit.Mvvm のバージョンが ver8 になりそうなのでpreviewをご紹介。
ver7の軽さはそのまま。ver8 の目玉はMVVMコードの記述簡易化です。
ver7の紹介記事はこちら
https://qiita.com/hqf00342/items/40a753edd8e37286f996
2024年12月15日追記
ver8.4以降でC#13の部分プロパティを利用した変更通知プロパティに対応したため反映しています。
新しいViewModelの記述方法
早速ですが新旧の書き方比較をしてみます。
最初に旧来(ver7)の方法で、変更通知できる Name
プロパティ と GreetCommand
コマンドを書いてみます。
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で書くと以下のコードになります。
[INotifyPropertyChanged]
internal partial class Vm
{
[ObservableProperty]
private string name;
[RelayCommand]
private void Greet(string user) {
Debug.WriteLine($"Hello {user}!");
}
}
ずいぶんと短くなりました。作ったプロパティやコマンドすら見当たらないですが、1つずつ説明していきます。
コードを簡潔に
C#の進化とともにMVVMのコード記述量は減りましたがそれでもまだ面倒でスニペットや便利なライブラリが良く使われます。海外では ReactiveUI、日本では ReactivePropertyあたりでしょうか。 Fody のようにILを直接書き換えるのも面白い試みです。
(興味ある方は @soi さんの INotifyPropertyChangedプロパティ実装方法まとめ C#3からC#7、Fodyも をご覧ください)
今回のMVVM Toolkit ver8では roslyn のソースジェネレータで解決してます。
面倒なコードはコンパイラが生成しているのでライブラリにありがちなオーバヘッドもありません。
ViewModelクラス定義
ver7までは ObservableObject
(他ライブラリでいうBindableBase) を継承していました。
internal class Vm : ObservableObject
{
}
上の書き方でもOKですが、ver8からINotifyPropertyChanged
属性で作れるようになります。またソースジェネレータのために partial
にします。
[INotifyPropertyChanged]
internal partial class Vm
{
}
C# は多重継承ができないため、データバインディング用に基底クラスを使うとほかのクラスを継承できなかった問題が解決します。
ICommandの記述
ICommandはこれまで以下のように実装していました。
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」属性に変更)
[RelayCommand]
private void Greet(string user)
{
Debug.WriteLine($"Hello {user}!");
}
XAMLからの使い方は変わりません。
<Button Content="Greet" Command="{Binding GreetCommand}" CommandParameter="test" />
変更通知プロパティ
INotifyPropertyChanged実装プロパティはいままでは以下のように記述していました。C#の進化とライブラリによってだいぶ短く書けるようになりました。
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
ver8からは以下のように書けます。
// ver8.0
// Name プロパティが自動生成される。
[ObservableProperty]
private string name;
さらにver8.4で部分プロパティに対応しました!
//ver8.4以降はプロパティを書けます
[ObservableProperty]
public partial string Name { get; set; };
ver8.0はバッキングフィールド name
を書いていましたが、ver8.4以降はName
プロパティを記述できるようになりました。ただしC#13時点では以下の注意点が必要です。
- c#13(.NET 9)に field キーワードが盛り込まれなかったため、csprojファイルに
<LangVersion>preview</LangVersion>
が必要です。 - プロパティに
pertial
キーワードが必要です。
XAMLでの使い方は変わりません。
<TextBlock Text="{Binding Name}" />
また、変更通知プロパティで変更前と変更後に処理を挟みたい時があります。Onプロパティ名Changing()
、Onプロパティ名Changed()
というメソッドを用意しておくと勝手に呼び出されるようになります。(詳しくはpreview3の紹介記事を参照)
[ObservableProperty]
private string name;
partial void OnNameChanging(string name)
=>Console.WriteLine($"Nameプロパティが {name} に変更されようとしてます");
partial void OnNameChanged(string name)
=>Console.WriteLine($"Nameプロパティが {name} に変更されました");
おわりに
以下ver8.0時点の感想はコメントアウト、継続的に改善されるライブラリはいいですね。
MVVMのコード記述が減るのは大歓迎ですが、この書き方で良いのかは疑問が残ります。僕らが望んでいたのは表(プロパティ等)を書いたら裏(backing-field等)を自動生成してくれる、だったのですが逆になってます。確かにソースジェネレータならこうなるよな、とは思うのですが・・。コード書く人はVisualStudioのインテリセンスで何とかなりますが、コードを読む人は自動生成された表側を推測しないといけないのが難点です。~~
→2年越しにver8.4で部分プロパティに対応したため、読みやすさも改善したと思われます。C#13に未導入のfield機能を使わなくても実装出来たろうに、と思うことはありますが。
ver8 の自動生成機能はソースジェネレータを利用しているため roslyn 4.x(VisualStudioならば2022)以降が必要になります。環境が用意できない場合でもソースジェネレータ以外の機能は使えます。
Messenger、コレクション系の通知機能も増えていますが正式リリース時に気が向いたら書きたいと思います。