0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WPF / CommunityToolkit.Mvvm: IMessenger 活用

Posted at

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 で注入したもの)を使う。
  • メッセージ型を用途ごとに分ける: CloseWindowMessageEmployeeSelectedMessage のように 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 インスタンスを共有し、ライフサイクルとスレッドに注意して使いましょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?