mvvm-toolkit-messenger について
CommunityToolkit.Mvvm を使う .NET 開発者向けに、Messenger で ViewModel などのオブジェクト間を疎結合に通信する方法を解説します。弱参照、ライフサイクル、チャネル、リクエスト/返信を扱います。
主な機能
- 01弱参照・強参照 Messenger
- 02メッセージの登録とブロードキャスト
- 03トークンによるチャネル分離
- 04同期・非同期のリクエスト/返信
- 05ObservableRecipient のライフサイクル管理
活用シーン
- 01ViewModel 間でログインやテーマ変更を通知
- 02ウィンドウ単位でメッセージを分離
- 03オブジェクト間のリクエストと返信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill mvvm-toolkit-messengerインストール後、Claude Code で/mvvm-toolkit-messengerを入力して読み込まれたか確認します。
同梱ファイルreferences/messenger-patterns.md
詳細
CommunityToolkit.Mvvm Messenger
ViewModel(または任意のオブジェクト)間で、共有参照グラフを強制することなくパブリッシュ/サブスクライブ方式のメッセージングを行います。CommunityToolkit.Mvvm 8.x の一部です。
要約。 既定では
WeakReferenceMessenger.Defaultを使用します。(recipient, message)ラムダとstatic修飾子を使ってハンドラーを登録し、thisをキャプチャしないようにします。ObservableRecipientを継承し、アクティブ化/非アクティブ化時にIsActiveを切り替えると、登録と登録解除が自動的に行われます。
このスキルを使用する場面
- 互いの参照を保持せずに、2つ以上の ViewModel がイベント(ログイン、テーマ変更、保存、ナビゲーション)に反応する必要がある場合
- ある ViewModel が別の VM に値を要求する必要がある場合(リクエスト/返信)
- チャネル トークンでイベントの範囲をサブシステムまたはウィンドウに限定する場合
- 「ハンドラーがまったく呼び出されない」問題や、弱参照の受信者のライフタイムに関する問題を診断する場合
ソースジェネレーター、基底クラス、コマンドについては、mvvm-toolkit スキルを参照してください。DI の設定(IMessenger インスタンスの登録)については、mvvm-toolkit-di を参照してください。
実装を選択する
| 型 | 使用する場面 |
|---|---|
WeakReferenceMessenger.Default | 既定。 受信者は弱参照で保持されるため、登録中でも GC の対象になります。内部のトリミングはフル GC 時に実行されるため、手動で Cleanup() を呼び出す必要はありません。 |
StrongReferenceMessenger.Default | プロファイラーでメッセンジャーがホットスポットとなっており、割り当てが問題になる場合。受信者は Unregister するまで固定されます。登録解除を忘れるとメモリリークが発生します。 |
カスタム IMessenger インスタンス | ウィンドウ単位またはスコープ単位(例:アプリのウィンドウごとに1つのメッセンジャー)。直接構築し、DI 経由で注入します。 |
ObservableRecipient のパラメーターなしコンストラクターは
WeakReferenceMessenger.Default を使用します。別の IMessenger を
コンストラクターに渡すと、既定値を上書きできます。
メッセージを定義する
ツールキットには基底クラスが用意されていますが、どのクラスでも使用できます。
using CommunityToolkit.Mvvm.Messaging.Messages;
// Single-payload broadcast
public sealed class LoggedInUserChangedMessage(User user)
: ValueChangedMessage<User>(user);
// Custom shape (records are great for this)
public sealed record ThemeChangedMessage(AppTheme NewTheme);
// Empty signal
public sealed record RefreshRequestedMessage;
受信者を登録する
ラムダ形式(推奨)
WeakReferenceMessenger.Default.Register<MyViewModel, ThemeChangedMessage>(
this,
static (recipient, message) => recipient.OnThemeChanged(message.NewTheme));
static 修飾子を使うと、意図しないクロージャーの割り当てを防ぎ、ラムダから
this を参照しないようにできます。代わりに recipient パラメーターを使います。
IRecipient<TMessage> インターフェイス形式
public sealed class MyViewModel : ObservableRecipient,
IRecipient<ThemeChangedMessage>,
IRecipient<RefreshRequestedMessage>
{
public void Receive(ThemeChangedMessage message) { /* ... */ }
public void Receive(RefreshRequestedMessage message) { /* ... */ }
}
ObservableRecipient.OnActivated() は Messenger.RegisterAll(this) を呼び出し、
その型が実装するすべての IRecipient<T> インターフェイスを登録します。
ObservableRecipient を使用しない場合は、手動で登録します。
WeakReferenceMessenger.Default.RegisterAll(this);
メッセージを送信する
WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));
// Empty payloads use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();
チャネル(トークン)
トークン(int、string、Guid など、等値比較可能な任意の値)を使って、
メッセージの範囲をサブシステムまたはウィンドウに限定します。
const int LeftPaneChannel = 1;
WeakReferenceMessenger.Default.Register<MyViewModel, RefreshRequestedMessage, int>(
this, LeftPaneChannel,
static (r, _) => r.RefreshLeft());
WeakReferenceMessenger.Default.Send(new RefreshRequestedMessage(), LeftPaneChannel);
トークンなしで送信されたメッセージは、既定の共有チャネルを使用します。 チャネルを指定して登録された受信者には配信されません。
リクエスト/返信
受信者が送信者に値を返す、問い合わせ形式のシナリオでは、
RequestMessage<T> ファミリーを使用します。
同期リクエスト
public sealed class CurrentUserRequest : RequestMessage<User> { }
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.CurrentUser));
User user = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
CurrentUserRequest から User への暗黙的な変換は、どの受信者も Reply を
呼び出していない場合に例外をスローします。最初にメッセージを取得して確認します。
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
User user = request.Response;
非同期リクエスト
public sealed class CurrentUserRequest : AsyncRequestMessage<User> { }
WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
this,
static (r, m) => m.Reply(r.GetCurrentUserAsync()));
User user = await WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
コレクションリクエスト(ファンイン)
CollectionRequestMessage<T> と AsyncCollectionRequestMessage<T> は、
応答したすべての受信者からの Reply を収集します。
public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }
var docs = WeakReferenceMessenger.Default.Send<OpenDocumentsRequest>();
foreach (Document doc in docs) { /* ... */ }
ライフサイクル
WeakReferenceMessenger を使用していても、受信者の破棄時には明示的に登録解除してください。
無効になったエントリがトリミングされ、パフォーマンスが向上します。
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage, int>(this, LeftPaneChannel);
WeakReferenceMessenger.Default.UnregisterAll(this);
ObservableRecipient.OnDeactivated() は、IsActive が false に切り替わると
自動的に登録解除を行います。アクティブ化フックで値を設定します。
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
ViewModel.IsActive = true;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
ViewModel.IsActive = false;
base.OnNavigatedFrom(e);
}
よくある落とし穴
- ラムダで
thisをキャプチャする。(r, m) => OnX(m)は暗黙的にthisをキャプチャし、クロージャーを割り当てて、ライフタイムを分かりにくくします。必ずstaticを付けて(r, m) => r.OnX(m)を使います。 Unregisterを使わずに受信者を強参照する。StrongReferenceMessengerでは、受信者(およびそのオブジェクトグラフ全体)が 永久に保持されます。ObservableRecipientを継承する(OnDeactivatedで自動的に登録解除されます)か、UnregisterAll(this)を呼び出します。- 継承されたメッセージ型。
BaseMessageに登録されたハンドラーは、DerivedMessage : BaseMessageに対しては呼び出されません。具象型ごとに登録してください。 - 誤ったメッセンジャーインスタンス。
WeakReferenceMessenger.Defaultで送信し、 ウィンドウごとに注入されたメッセンジャーで登録すると、メッセージは届きません。同じIMessengerを どこでも使います(通常はObservableRecipient(messenger)を介して注入します)。 OnActivatedが実行されない。ObservableRecipientは、IsActiveがfalseからtrueに切り替わったときにのみ、IRecipient<T>ハンドラーを登録します。- スレッドをまたぐ更新。 メッセンジャーはスレッドに依存しません。ハンドラーが UI を更新する場合は、手動で
UI スレッドに処理を振り分けます
(
DispatcherQueue.TryEnqueue/Dispatcher.BeginInvoke)。
複数のメッセンジャー(ウィンドウごとのスコープ)
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
適切な IMessenger を ViewModel のコンストラクターに注入します。
public sealed partial class WindowViewModel(IMessenger messenger)
: ObservableRecipient(messenger) { }
これにより、ブロードキャストを単一のウィンドウに限定できます。複数ウィンドウの デスクトップアプリ(WinUI 3、WPF、MAUI desktop、Avalonia)で役立ちます。
参考資料
| トピック | ファイル |
|---|---|
| 詳細な解説(チャネルやライフサイクルの例、診断) | references/messenger-patterns.md |
外部資料: