- 2022/12/14
- 「WeakとStrong」の節に少し追記
はじめに
.NET Community Toolkit の v8.0.0 が8月にリリースされました。
これは雑に言うと、Windows開発用のWindows Community ToolkitからWindows 固有でないAPIが独立して作成された最初のバージョンです。
上記Toolkitに、CommunityToolkit.Mvvmが含まれています。
.NET 6 で Incremental Source Generator(ISG)が登場し、多くのボイラープレートコードが高速に生成できるようになりました。
CommunityToolkit.Mvvmでも同様にISGを活用し、多くのコード生成を実現しています。
※ちなみにコード生成機能自体は7.1.0でもあったようです。
本稿ではv8のうち、特にコード生成に関連する部分(の一部)を紹介します。
自動生成コードとある程度格闘できる人向けの記事ということで、記事内ではXamlは原則省略します。
基本的な使い方
NotifyPropertyChangedを実装したViewModelを作成する
ViewModelとしたいクラスにINotifyPropertyChanged
属性とpartial
を付与するだけです。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
}
監視可能なプロパティを作成する
バッキングフィールドを先に記述し、そのバッキングフィールドへObservableProperty
属性を付与します。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
+ [ObservableProperty]
+ private string _text = string.Empty; //Text というプロパティが生成される
}
生成されるプロパティは以下の通り1です。
inheritdoc
が付与されるのはかなり嬉しいポイントです。
クリックして展開
partial class MainWindowViewModel
{
/// <inheritdoc cref="_text"/>
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
_text = value;
OnPropertyChanged(new PropertyChangedEventArgs("Text"));
}
}
}
}
生成されるプロパティの名前
プロパティの名前は、バッキングフィールドの名前を元に以下の通り決定されます。
- プレフィックスの削除
-
m_
で始まる場合はm_
を削除 -
_
で始まる場合は一連のアンダーバーを削除
-
- 残った文字列の先頭1文字を大文字に
つまり、以下のフィールドに対してはText
というプロパティが生成されます。
m_text
, _text
, ____text
, text
監視可能なプロパティに依存したプロパティの変更を通知する
NotifyPropertyChangedFor
属性を利用することで、自身が更新された際に他のプロパティの更新通知を投げることができます。
タイトル含め少しわかりにくいですが、コードを見ればシンプルです。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(TextUpper))] //Textの変更時に、OnPropertyChanged("TextUpper")がコールされる
private string _text = string.Empty; //Text というプロパティが生成される
+ public string TextUpper => _text.ToUpper();
}
生成されるプロパティは以下の通り1です。
クリックして展開
TextUpper
への変更通知が追加されています。
partial class MainWindowViewModel
{
/// <inheritdoc cref="_text"/>
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
_text = value;
OnPropertyChanged(new PropertyChangedEventArgs("Text"));
+ OnPropertyChanged(new PropertyChangedEventArgs("TextUpper"));
}
}
}
}
コマンドを作成する
コマンドにしたい関数へ、RelayCommand
属性を付与します。
引数にはCommandParameter
で指定した値が利用されます。
なお、返り値はvoid
である必要があります。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
//...
+ [RelayCommand]
+ private void SyncMethod() //SyncMethodCommand が生成される
+ {
+ //...
+ }
+ [RelayCommand]
+ private void SyncMethodWithParam(string str) //SyncMethodWithParamCommand が生成される
+ {
+ //...
+ }
}
生成されるコマンドは以下の通り1です。
クリックして展開
partial class MainWindowViewModel
{
/// <summary>The backing field for <see cref="SyncMethodCommand"/>.</summary>
private RelayCommand? syncMethodCommand;
/// <summary>Gets an <see cref="IRelayCommand"/> instance wrapping <see cref="SyncMethod"/>.</summary>
public IRelayCommand SyncMethodCommand
=> syncMethodCommand ??= new RelayCommand(new Action(SyncMethod));
/// <summary>The backing field for <see cref="SyncMethodWithParamCommand"/>.</summary>
private RelayCommand<string>? syncMethodWithParamCommand;
/// <summary>Gets an <see cref="IRelayCommand{T}"/> instance wrapping <see cref="SyncMethodWithParam"/>.</summary>
public IRelayCommand<string> SyncMethodWithParamCommand
=> syncMethodWithParamCommand ??= new RelayCommand<string>(new Action<string>(SyncMethodWithParam));
}
生成されるコマンドの名前
コマンドの名前は、関数の名前を元に以下の通り決定されます。
- プレフィックスの削除
-
On
で始まる場合はOn
を削除
-
- 残った文字列の末尾に
Command
を付与
非同期コマンドを作成する
基本的に、同期コマンドと同じです。
返り値はTask
かTask
を継承した型である必要があります。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
//...
+ [RelayCommand]
+ private async Task AsyncMethodAsync() //AsyncMethodCommand が生成される
+ {
+ }
+ [RelayCommand]
+ private async Task AsyncMethodWithParamAsync(string str) //AsyncMethodWithParamCommand が生成される
+ {
+ }
}
生成される非同期コマンドは以下の通り1です。
クリックして展開
partial class MainWindowViewModel
{
/// <summary>The backing field for <see cref="AsyncMethodCommand"/>.</summary>
private AsyncRelayCommand? asyncMethodCommand;
/// <summary>Gets an <see cref="IAsyncRelayCommand"/> instance wrapping <see cref="AsyncMethodAsync"/>.</summary>
public IAsyncRelayCommand AsyncMethodCommand
=> asyncMethodCommand ??= new AsyncRelayCommand(new Func<Task>(AsyncMethodAsync));
/// <summary>The backing field for <see cref="AsyncMethodWithParamCommand"/>.</summary>
private AsyncRelayCommand<string>? asyncMethodWithParamCommand;
/// <summary>Gets an <see cref="IAsyncRelayCommand{T}"/> instance wrapping <see cref="AsyncMethodWithParamAsync"/>.</summary>
public IAsyncRelayCommand<string> AsyncMethodWithParamCommand
=> asyncMethodWithParamCommand ??= new AsyncRelayCommand<string>(new Func<string, Task>(AsyncMethodWithParamAsync));
}
生成される非同期コマンドの名前
非同期コマンドの名前は、関数の名前を元に以下の通り決定されます。
- プレフィックスの削除
-
On
で始まる場合はOn
を削除
-
- サフィックスの削除
-
Async
で終わる場合はAsync
を削除
-
- 残った文字列の末尾に
Command
を付与
非同期コマンドのキャンセル
非同期コマンドにするメソッドの引数にCancellationToken
を指定することで、実行中のコマンドを容易にキャンセルすることができます。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
//...
+ [RelayCommand]
+ private async Task CancelableAsyncMethodAsync(CancellationToken token) //CancelableAsyncMethodCommand が生成される
+ {
+ }
+ [RelayCommand]
+ private async Task CancelableAsyncMethodWithParamAsync(string str, CancellationToken token) //CancelableAsyncMethodWithParamCommand が生成される
+ {
+ //tokenをチェックしながらの重い処理...
+ }
}
実際にキャンセルするには対応するCommandのCancel
を呼び出します。
例えばキャンセルボタンを設ける場合、以下のように記述します。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
//...
[RelayCommand]
private async Task CancelableAsyncMethodWithParamAsync(string str, CancellationToken token) //CancelableAsyncMethodWithParamCommand が生成される
{
//tokenをチェックしながらの重い処理...
}
+ [RelayCommand]
+ private void Cancel()
+ {
+ CancelableAsyncMethodWithParamCommand.Cancel(); //これ
+ }
}
やや複雑な使い方
コマンドの実行可否を制御する
いわゆるCanExecuteの使い方です。
「基本的な」に書くか少し迷いましたが、ぱっと見では属性間の関係性が分かりづらいため、こちらに書きます。
- まず、コマンドの実行可否を返すプロパティ、またはメソッドを定義します
⇒ 下記の例だとCanExecuteSyncMethod
- 対象コマンドの
RelayCommand
属性に1.で定義したプロパティ or メソッドを指定します
⇒ 同、[RelayCommand(CanExecute = nameof(CanExecuteSyncMethod))]
- 最後に、実行可否に影響するプロパティに
NotifyCanExecuteChangedFor
属性を付与します
⇒ 同、[NotifyCanExecuteChangedFor(nameof(SyncMethodCommand))]
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TextUpper))]
+ [NotifyCanExecuteChangedFor(nameof(SyncMethodCommand))] //3. このプロパティが影響するCommandを指定する
private string _text = string.Empty;
+ private bool CanExecuteSyncMethod => Text.Length > 0; //1. メソッドでも可。フィールドは不可
+ //private bool CanExecuteSyncMethod() => Text.Length > 0; //メソッド版
- [RelayCommand]
+ [RelayCommand(CanExecute = nameof(CanExecuteSyncMethod))] //2. 実行可否のプロバイダを指定する
private void SyncMethod() //SyncMethodCommand が生成される
{
}
}
生成されるプロパティ及びコマンドは以下の通り1変化します。
クリックして展開
プロパティにはNotifyCanExecuteChanged
が追加されています。
partial class MainWindowViewModel
{
/// <inheritdoc cref="_text"/>
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
//...
+ SyncMethodCommand.NotifyCanExecuteChanged();
}
}
}
}
コマンドの生成時に、CanExecuteSyncMethod
が引き渡されるようになります。
partial class MainWindowViewModel
{
/// <summary>The backing field for <see cref="SyncMethodCommand"/>.</summary>
private RelayCommand? syncMethodCommand;
/// <summary>Gets an <see cref="IRelayCommand"/> instance wrapping <see cref="SyncMethod"/>.</summary>
public IRelayCommand SyncMethodCommand
- => syncMethodCommand ??= new RelayCommand(new Action(SyncMethod));
+ => syncMethodCommand ??= new RelayCommand(new Action(SyncMethod), () => CanExecuteSyncMethod);
}
プロパティの変更前後に処理を挟む
ObservableProperty
を用いて作成した監視可能なプロパティが、バッキングフィールドへ値を反映する前後に処理を差し込むことができます。
「監視可能なプロパティの作成」では省略しましたが、以下のような監視可能プロパティに対し、、、
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
[ObservableProperty]
private string _text = string.Empty;
}
実装を持たないpartialメソッドが2つ作成されます。(以下だと、OnTextChanging
とOnTextChanged
)
internal partial class MainWindowViewModel
{
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
+ OnTextChanging(value); //ここで呼び出し
_text = value;
+ OnTextChanged(value); //ここで呼び出し
OnPropertyChanged(new PropertyChangedEventArgs("Text"));
}
}
}
/// <summary>Executes the logic for when <see cref="Text"/> is changing.</summary>
+ partial void OnTextChanging(string value); //こいつと
/// <summary>Executes the logic for when <see cref="Text"/> just changed.</summary>
+ partial void OnTextChanged(string value); //こいつ
}
これらpartialメソッドを実装することで、処理を差し込みます。
値変更前後でデバッグ出力をするには、以下のように実装します。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
[ObservableProperty]
private string _text = string.Empty;
//このメソッドをコールすると、
private void Test()
{
Text = "After";
}
+ partial void OnTextChanging(string value)
+ {
+ System.Diagnostics.Debug.WriteLine($"Changing: Text = {Text}, value = {value}");
+ // => Changing: Text = , value = After
+ }
+ partial void OnTextChanged(string value)
+ {
+ System.Diagnostics.Debug.WriteLine($"Changed: Text = {Text}, value = {value}");
+ // => Changed: Text = After, value = After
+ }
}
ちなみにpartialメソッドを実装しない場合、プロパティ更新時のpartialメソッド呼び出しはコンパイラーによって削除されます。
値のバリデーション
バリデーションを行うには、ViewModelにObservableValidator
を継承させます。
これによりINotifyPropertyChanged
インターフェースが重複してしまうため、INotifyPropertyChanged
属性は削除します。
さらにバリデーション対象となるプロパティ(のバッキングフィールド)へNotifyDataErrorInfo
属性を付与し、チェックしたい内容に応じてRequired
属性などのValidationAttribute
を継承した属性を付与します。
とはいえ、コード生成に関連するのはNotifyDataErrorInfo
属性のみで、その他は従来の(or WPF本来の)ものです。
- [INotifyPropertyChanged]
- internal partial class MainWindowViewModel
+ internal partial class MainWindowViewModel : ObservableValidator
{
+ [Required]
+ [NotifyDataErrorInfo]
[ObservableProperty]
private string _text = string.Empty;
}
生成されるプロパティは以下の通り1変化します。
クリックして展開
NotifyDataErrorInfo
属性を付けたことにより、ValidateProperty
が呼び出されています。
また、バッキングフィールドへ付与したRequired
がそのまま転記されています。
partial class MainWindowViewModel
{
/// <inheritdoc cref="_text"/>
+ [RequiredAttribute()]
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
OnTextChanging(value);
_text = value;
+ ValidateProperty(value, "Text");
OnTextChanged(value);
OnPropertyChanged(new PropertyChangedEventArgs("Text"));
}
}
}
}
従来のバリデーションと同様にValidationAttribute
を継承した独自属性を定義するか、CustomValidation
属性を用いて独自のバリデーションを仕込むこともできます。
プロパティの変更をブロードキャストする
ViewModelにObservableRecipient
属性を付与し、変更をブロードキャストしたいプロパティにNotifyPropertyChangedRecipients
属性を付与します。
+ [ObservableRecipient]
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
+ [NotifyPropertyChangedRecipients]
[ObservableProperty]
private string _text = string.Empty;
}
生成されるプロパティは以下の通り1変化します。
クリックして展開
Broadcast
の呼び出しが追加されます。
partial class MainWindowViewModel
{
/// <inheritdoc cref="_text"/>
public string Text
{
get => _text;
set
{
if (!EqualityComparer<string>.Default.Equals(_text, value))
{
OnTextChanging(value);
_text = value;
OnTextChanged(value);
OnPropertyChanged(new PropertyChangedEventArgs("Text"));
+ Broadcast(__oldValue, value, "Text");
}
}
}
}
ブロードキャストは後述するメッセンジャーを用いて行われますが、このメッセンジャーは自動生成されるコンストラクタで初期化されます。
具体的には、以下のような1コンストラクタが生成されます。
partial class MainWindowViewModel
{
public MainWindowViewModel() : this(WeakReferenceMessenger.Default)
{
}
public MainWindowViewModel(IMessenger messenger)
{
Messenger = messenger;
}
}
ここには少し罠があって、独自で実装したコンストラクタが存在する場合、上記のコンストラクタは生成されません。
そのため、コンストラクタを自力実装する場合(≒たいていの場合)、Messenger
プロパティにインスタンスをセットしてあげる必要があります。
なおこの罠は、ObservableRecipient
を継承することでも回避できます。
さて、最終的にメッセージは以下のよう1に送信されます。(T
は通知されるプロパティの型)
void Broadcast()
{
PropertyChangedMessage<T> message = new(this, propertyName, oldValue, newValue);
Messenger.Send<PropertyChangedMessage<T>>(message);
}
詳細は後述しますが、受信は以下のように行います。
ここではPropertyChangedMessage<T>
が送受信されることが分かれば十分です。
Messenger.Register<MainWindowViewModel, PropertyChangedMessage<string>>(this, static (recipient, message) =>
{
//NotifyPropertyChangedを購読するのと同じように書ける
switch (message.PropertyName)
{
case nameof(Text):
//なんかする
break;
}
});
その他
メッセンジャー
コード生成を利用しないAPIで、IMessenger
インターフェースとして提供されており、オブジェクト間のメッセージ交換をサポートします。
組み込みのIMessenger
を利用する場合、WeakReferenceMessenger
クラスかStrongReferenceMessenger
クラスのDefault
静的プロパティから取得できます。
必要に応じて独自にインスタンス化(new)したものを利用することもできます。
受信と送信
送受信するメッセージの定義
まず、必要に応じてメッセージを定義します。
CommunityToolkit.Mvvm.Messaging.Messages.ValueChangedMessage<T>
を継承すると簡単です。
internal class MyMessage : ValueChangedMessage<string>
{
public MyMessage(string value) : base(value) { } //インテリセンス任せ
}
送信
ボタンクリックでメッセージを送信には、以下のようにすればOKです。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
[RelayCommand]
private void SendMessage()
{
+ WeakReferenceMessenger.Default.Send<MyMessage>(new("Mesage from MainWindowViewModel")); //送信!
}
}
受信
上記メッセージを別ウィンドウで受信する場合、以下のように記述します。
public partial class SubWindow : Window
{
public SubWindow()
{
InitializeComponent();
+ WeakReferenceMessenger.Default.Register<SubWindow, MyMessage>(this, static (s, e) =>
+ {
+ //SendMessageCommandをExecuteすると、
+ //「Received: Mesage from MainWindowViewModel」と出力
+ System.Diagnostics.Debug.WriteLine($"Received: {e.Value}");
+ });
}
}
他のパターン
手間ですが、IRecipient<T>
を実装したクラスを受信者として登録することもできます。
internal class MyRecipient : IRecipient<MyMessage>
{
public void Receive(MyMessage message)
{
Debug.WriteLine($"MyRecipient Received: {message.Value}");
}
}
public partial class SubWindow : Window
{
//WeakRefの場合、MyRecipient参照を握っておく必要あり
+ private readonly MyRecipient _recipient = new();
public SubWindow()
{
InitializeComponent();
//ここでMyRecipientをnewした場合、WeakReferenceMessengerが強い参照を持たないため
//コンストラクタを抜けたタイミングでMyRecipientがGCの対象となってしまう。
//WeakReferenceMessenger.Default.Register(new MyRecipient()); ダメ!
+ WeakReferenceMessenger.Default.Register(_recipient);
WeakReferenceMessenger.Default.Register<SubWindow, MyMessage>(this, static (s, e) =>
{
//SendMessageCommandをExecuteすると、
//「Message: Mesage from MainWindowViewModel」と出力
System.Diagnostics.Debug.WriteLine($"Message: {e.Value}");
});
}
}
コメントに書いていますが、WeakReferenceMessenger
の場合は後述の理由で少し注意が必要です。
リクエスト
単方向の送受信だけでなく、レスポンスを要求することもできます。
送受信するメッセージの定義
こちらはCommunityToolkit.Mvvm.Messaging.Messages.RequestMessage<T>
を継承すると簡単です。
※非同期版のAsyncRequestMessage<T>
も存在します。
internal class MyRequestMessage : RequestMessage<string>
{
public DateTime RequestedAt { get; }
//例ではDateTimeを渡す
public MyRequestMessage(DateTime requestedAt)
{
RequestedAt = requestedAt;
}
}
リクエスト
ボタンクリックでリクエストするには、以下のようにすればOKです。
[INotifyPropertyChanged]
internal partial class MainWindowViewModel
{
//..
[RelayCommand]
private void SendRequest()
{
//リクエスト送信
MyRequestMessage response = Messenger.Send<MyRequestMessage>(new(DateTime.Parse("2022/12/13")));
Debug.WriteLine($"RequestedAt = {response.RequestedAt}, Response = {response.Response}");
// => 「RequestedAt = 2022/12/13 0:00:00, Response = Response from SubWindow.」
}
}
レスポンス
上記メッセージを別ウィンドウで受信しレスポンスを返すには、以下のように記述します。
public partial class SubWindow : Window
{
public SubWindow()
{
//...
+ WeakReferenceMessenger.Default.Register<SubWindow, MyRequestMessage>(this, static (recipient, message) =>
+ {
+ //RequestMessage<T>のTに対応する型を返す
+ message.Reply("Response from SubWindow.");
+ });
}
}
WeakとStrong
IMessenger
の実装にはWeakReferenceMessenger
とStrongReferenceMessenger
の二つが存在します。
WeakReferenceMessenger
は名前の通り弱参照を利用しており、受信者がGCされることを阻害しません。
例えば以下のようにWindow
のコードビハインドでメッセージを受信するケースだと、StrongReferenceMessenger
の場合はWindow
がクローズされたあともSubWindow
のデストラクタは呼ばれません。一方、WeakReferenceMessenger
の場合はデストラクタが呼ばれます。
この性質は、小規模ダイアログを後始末する手間や、ViewModelの後始末のし辛さに対しては便利です。
public partial class SubWindow : Window
{
public SubWindow()
{
InitializeComponent();
//thisへの参照をやんわり掴む(GCを阻害しない)
//WeakReferenceMessenger.Default.Register<SubWindow, PropertyChangedMessage<string>>(this, static (_, _) => { });
//thisへの参照をがっちり掴む
StrongReferenceMessenger.Default.Register<SubWindow, PropertyChangedMessage<string>>(this, static (_, _) => { });
}
//StrongReferenceMessengerを用いた場合、Windowをクローズしても以下は実行されない(当然、GC.Collect()を呼んでもダメ)
//WeakReferenceMessengerだと、GCが走ったタイミングで"Destructed"と表示される
~SubWindow()
{
System.Diagnostics.Debug.WriteLine("Destructed");
}
}
注意点
確かに便利なのですが、上の方で触れたIRecipient<T>
を利用して別途受信者を準備するケースなどでは、意図せず受信者が消滅してしまうこともあり取扱いには注意が必要です。
「起動してしばらくは動くのに、しばらくすると(≒GCのタイミングが来ると)受信しなくなる」といった、問題に気が付きづらい不具合に繋がります。
(まぁ、StrongReferenceMessenger
によるメモリリークもそれはそれで気付きにくいのですが。。。)
参考リンク
Announcing .NET Community Toolkit 8.0! MVVM, Diagnostics, Performance, and more!