CommunityToolkit.Mvvm の IMessenger は、ViewModel 間や View への通知を疎結合にやり取りするための仕組みです。ここでは Qiita 向けに、基本のクローズ通知と、ドメインモデルを運ぶメッセージの例、DI 登録例、そして受信後の UI 更新パターンをまとめます。
0. IMessenger の取得方法
- シンプル:
WeakReferenceMessenger.Defaultをそのまま使う。 - DI で注入:
IMessengerをコンストラクタで受け取り、同じインスタンスを送受信用に使う(推奨)。
public class ViewModelA
{
private readonly IMessenger _messenger;
public ViewModelA(IMessenger messenger) => _messenger = messenger;
}
DI 登録例(App.xaml.cs など)
namespace MvvmSample
{
public partial class App : Application
{
private ServiceProvider? _provider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
// リポジトリ
services.AddSingleton<IEmployeeRepository, SqliteEmployeeRepository>();
services.AddSingleton<ISqliteEmployeeRepo, SqliteEmployeeRepo>();
// Messenger をアプリ全体で共有する Singleton として登録
services.AddSingleton<IMessenger, WeakReferenceMessenger>();
// ViewModel
services.AddTransient<MainViewModel>();
services.AddTransient<InsertViewModel>();
// View
services.AddSingleton<MainWindow>();
services.AddTransient<InsertView>();
// WindowFactory / WindowService
services.AddTransient<Func<InsertView>>(sp => () => sp.GetRequiredService<InsertView>());
services.AddSingleton<IWindowService, WindowService>();
_provider = services.BuildServiceProvider();
var window = _provider.GetRequiredService<MainWindow>();
window.Show();
}
}
}
- Point: 送信側・受信側とも DI から同じ
IMessengerを受け取ることで、WeakReferenceMessenger.Defaultに依存せずテストもしやすい。
1. ウィンドウを閉じるメッセージの基本形
Show() で開いたウィンドウを閉じたいときに使う定番のパターンです。
メッセージ定義
// Messages/CloseWindowMessage.cs
namespace DataGridSample.Messages;
public sealed record CloseWindowMessage;
送信(ViewModel)
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
public partial class SampleViewModel
{
[RelayCommand]
private void CloseCommand()
{
// 「このウィンドウを閉じて」という通知
WeakReferenceMessenger.Default.Send(new CloseWindowMessage());
// DI なら _messenger.Send(new CloseWindowMessage());
}
}
受信(Window コードビハインド)
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
public partial class SampleWindow : Window
{
public SampleWindow()
{
InitializeComponent();
Loaded += (_, __) => WeakReferenceMessenger.Default.Register<CloseWindowMessage>(this, OnCloseRequested);
Unloaded += (_, __) => WeakReferenceMessenger.Default.UnregisterAll(this);
}
private void OnCloseRequested(object recipient, CloseWindowMessage message)
{
Close();
}
}
XAML 例
<Button Content="閉じる" Command="{Binding CloseCommand}" />
2. ドメインモデルを運ぶメッセージ(ValueChangedMessage)
「ウィンドウを閉じて」ではなく「このドメインモデルを渡して」という用途なら、ValueChangedMessage<T> を継承したメッセージを使うと安全・簡潔です。以下は Employee を運ぶ例です。
メッセージ定義
// Messages/EmployeeSelectedMessage.cs
using CommunityToolkit.Mvvm.Messaging.Messages;
using DataGridSample.Models; // Employee の名前空間
namespace DataGridSample.Messages;
// Employee を運ぶメッセージ
public sealed class EmployeeSelectedMessage : ValueChangedMessage<Employee>
{
public EmployeeSelectedMessage(Employee value) : base(value) { }
}
送信(ViewModelA など)
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
using DataGridSample.Models;
public partial class ViewModelA
{
private readonly IMessenger _messenger;
public ViewModelA(IMessenger messenger) => _messenger = messenger;
// どこかで Employee を取得したときに送信
private void OnEmployeeLoaded(Employee employee)
{
_messenger.Send(new EmployeeSelectedMessage(employee));
}
}
受信(ViewModelB)
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
using DataGridSample.Models;
public partial class ViewModelB : IRecipient<EmployeeSelectedMessage>
{
public Employee? SelectedEmployee { get; private set; }
public ViewModelB(IMessenger messenger)
{
// このインスタンスを Recipient として登録
messenger.Register(this);
}
// 受信時に呼ばれる
public void Receive(EmployeeSelectedMessage message)
{
SelectedEmployee = message.Value;
// ここで画面用プロパティに反映したり、詳細画面を開いたりできる
}
}
3. 運用のポイント
-
同じ IMessenger インスタンスを共有すること: 送信側と受信側が同じインスタンス(
WeakReferenceMessenger.Defaultか DI で注入したもの)を使う。 -
メッセージ型を用途ごとに分ける:
CloseWindowMessage、EmployeeSelectedMessageのように 1 メッセージ 1 用途で強く型付けする。 - ライフサイクル管理: Window などでは Loaded/Unloaded で Register/Unregister し、メモリリークを防ぐ。
- スレッド: 受信は発行元スレッドで呼ばれる。UI 更新が必要なら Dispatcher を使う。
4. こんなときに使う
- モーダルではない画面間での「閉じて」「開いて」「選択した値を渡して」などの疎結合な通知。
- ViewModel 間でのデータ伝搬をイベントやシングルトンに頼らず、型安全に行いたいとき。
5. メッセージ受信後に UI を更新するには?
IMessenger でメッセージを受け取ったあとは、ViewModel のプロパティを更新し、XAML にバインドさせるのが基本です。
5-1. ViewModel のプロパティを更新する例
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
using DataGridSample.Models;
public partial class EmployeeDetailViewModel : ObservableObject, IRecipient<EmployeeSelectedMessage>
{
[ObservableProperty]
private string? name;
[ObservableProperty]
private int age;
[ObservableProperty]
private string? departmentName;
private readonly IMessenger _messenger;
public EmployeeDetailViewModel(IMessenger messenger)
{
_messenger = messenger;
_messenger.Register(this); // EmployeeSelectedMessage を受信する
}
// EmployeeSelectedMessage を受信したときに呼ばれる
public void Receive(EmployeeSelectedMessage message)
{
var emp = message.Value;
Name = emp.Name;
Age = emp.Age;
DepartmentName = emp.Department?.Name;
}
}
-
ObservableObject+[ObservableProperty]でプロパティ変更時に自動でPropertyChangedが発火し、UI が更新される。 -
Receive(...)の中では ViewModel の状態更新だけにとどめるとテストもしやすい。
5-2. XAML でバインドする
<StackPanel Margin="16">
<TextBlock Text="名前:" FontWeight="Bold" />
<TextBlock Text="{Binding Name}" Margin="0,0,0,8" />
<TextBlock Text="年齢:" FontWeight="Bold" />
<TextBlock Text="{Binding Age}" Margin="0,0,0,8" />
<TextBlock Text="部署:" FontWeight="Bold" />
<TextBlock Text="{Binding DepartmentName}" Margin="0,0,0,8" />
</StackPanel>
ViewModelA 側で EmployeeSelectedMessage を送信すると、EmployeeDetailViewModel.Receive(...) が呼ばれ、プロパティが更新され、その結果が自動的に UI に反映されます。
6. バックグラウンドスレッドからメッセージが届く場合の注意点
通常は UI スレッドから送るためそのまま更新で問題ありませんが、バックグラウンド処理から届く場合は UI スレッドに戻してから更新します。
public void Receive(EmployeeSelectedMessage message)
{
if (Application.Current.Dispatcher.CheckAccess())
{
ApplyEmployee(message.Value);
}
else
{
Application.Current.Dispatcher.Invoke(() =>
{
ApplyEmployee(message.Value);
});
}
}
private void ApplyEmployee(Employee emp)
{
Name = emp.Name;
Age = emp.Age;
DepartmentName = emp.Department?.Name;
}
- UI にバインドされたプロパティ更新は UI スレッドで行う。
-
CheckAccess()/Invoke()で安全にスレッドを切り替える。
7. ObservableCollection の更新例
リスト更新も同様で、ObservableCollection<T> を ViewModel に持たせ、受信時に更新します。
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using DataGridSample.Messages;
using DataGridSample.Models;
public partial class EmployeeListViewModel : ObservableObject, IRecipient<EmployeeSelectedMessage>
{
public ObservableCollection<Employee> Employees { get; } = new();
private readonly IMessenger _messenger;
public EmployeeListViewModel(IMessenger messenger)
{
_messenger = messenger;
_messenger.Register(this);
}
public void Receive(EmployeeSelectedMessage message)
{
var emp = message.Value;
// 例: 既に存在すれば更新、なければ追加
var existing = Employees.FirstOrDefault(x => x.Id == emp.Id);
if (existing is null)
{
Employees.Add(emp);
}
else
{
var index = Employees.IndexOf(existing);
Employees[index] = emp;
}
}
}
- ドメインモデルを受け取り、ViewModel のコレクションに反映する。
- XAML 側は
ItemsSource="{Binding Employees}"のようにバインドするだけで更新が UI に反映される。
以上を押さえておけば、IMessenger で UI の制御やドメインモデル伝搬をシンプルかつ安全に実装できます。メッセージを用途ごとに分け、同じ Messenger インスタンスを共有し、ライフサイクルとスレッドに注意して使いましょう。