プロジェクトの準備

この記事では、メッセージングの利用自体は簡単であることを実感してもらうため、コンソールアプリケーションでの、新規プロジェクトによるサンプルとなります。

Visual Studioから「新しいプロジェクト」で「C#」「Windows」経由で「コンソール アプリ」を選び、プロジェクト名は「MessagingTest」にして、ソリューションを作成してください。今回はプロジェクト名を変更しても問題ありません。

フレームワークは「.NET 9.0」を使用します。

MVVM Toolkfit の利用準備

ソリューションに対し、Visual Studioのメニュー「ツール」「NuGetパッケージマネージャ」から「ソリューションのNuGetパッケージの管理」を選び、「参照」タブから CommunityToolkit.Mvvm をインストールします。

(1)基本のメッセージング

まずはProgram.csを全削除し、以下の内容に置き換えます。この際、コピペでは身につかないとか言いません。プロジェクトを作って動かせば、見えてくるものもあるでしょう。

using CommunityToolkit.Mvvm.Messaging;

namespace MessageingTest
{
    /// <summary>
    /// 送信クラス(Main含む)
    /// </summary>
    public static class Sender
    {
        public static void Main()
        {
            // 保持する必要はないが、ここでは受信クラスを作る必要がある
            var _ = new Receiver();

            // 弱い参照でのメッセージング
            var message = new MyMessage("もりゃき", 12);
            WeakReferenceMessenger.Default.Send(message);

            // メッセージが出力されるまで待機
            Console.ReadKey();
        }
    }
    /// <summary>
    /// 受信クラス
    /// </summary>
    public class Receiver
    {
        public Receiver()
        {
            // 弱い参照での受信設定
            WeakReferenceMessenger.Default.Register<MyMessage>(this, HandleRegistrationInfo);
        }

        // 受信メッセージの処理メソッド
        private void HandleRegistrationInfo(object recipient, MyMessage message)
        {
            Console.WriteLine($"{message.Name} の所持金は {message.Value} 円。");
        }
    }

    /// <summary>
    /// メッセージクラス
    /// </summary>
    public class MyMessage
    {
        public string Name { get; set; } = string.Empty;
        public int Value { get; set; }

        public MyMessage() { }

        public MyMessage(string name, int value)
        {
            Name = name;
            Value = value;
        }
    }
}

こうすると、以下の出力になります。

もりゃき の所持金は 12 円。

送信クラスから、メッセージに送るデータ「もりゃき」「12」をMyMessageクラスのインスタンスで包んで、受信クラスReceiverに送って出力している訳です。

これについては、大体の流れを抑えて貰えればいいかなと思ってます。

(2)ラムダ式を使ったメッセージ受信

先ほどのReceiverクラスを以下の物に置き換えます。 HandleRegistrationInfo をラムダ式にしています。

    /// <summary>
    /// 受信クラス
    /// </summary>
    public class Receiver
    {
        public Receiver()
        {
            // 弱い参照での受信設定
            WeakReferenceMessenger.Default.Register<MyMessage>(this, (_, m) =>
                Console.WriteLine($"{m.Name} の所持金は {m.Value} 円。"));
        }
    }

元の引数object recipientは使わないので省略、MyMessage messagemに縮めました。出力は全く同じです。

もりゃき の所持金は 12 円。

随分スッキリしましたね。 実は、単純にスッキリする為ではなく、ラムダ式を使った方が速い のです。

(3)強い参照を使ったメッセージング

次はメッセージ送信にStrongReferenceMessengerを使います。送信クラス、受信クラス両方の変更が必要になります。

    /// <summary>
    /// 送信クラス(Main含む)
    /// </summary>
    public static class Sender
    {
        public static void Main()
        {
            using (var recv = new Receiver())
            {
                // 送信処理
                var message = new MyMessage("もりゃき", 12);
                // 強い参照でのメッセージング
                StrongReferenceMessenger.Default.Send(message);
            }
            // メッセージが出力されるまで待機
            Console.ReadKey();
        }
    }
    /// <summary>
    /// 受信クラス
    /// </summary>
    public class Receiver : IDisposable
    {
        public Receiver()
        {
            // 強い参照での受信設定
            StrongReferenceMessenger.Default.Register<MyMessage>(this, (_, m)
                => Console.WriteLine($"{m.Name} の所持金は {m.Value} 円。"));
        }

        private bool isDisposed = false;
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (isDisposed) return;
            if (disposing)
            {
                // 購読解除
                StrongReferenceMessenger.Default.Unregister<MyMessage>(this);
                Console.WriteLine("Unregister StrongReferenceMessenger");
            }
            isDisposed = true;
        }
    }

当然、以下の出力になります。

もりゃき の所持金は 12 円。

これが最速です! しかし、購読解除を必ずしなければならないため、使い勝手はあまり良くないでしょう。実はもりゃきも使ってません。

ReceiverUnregisterするのは決して推奨されていません!このように、Disposeパターンを使いましょう!

弱い参照では購読解除が厳密には必要ないという記載が公式にあります。購読解除は弱い参照でもパフォーマンス的にはやった方がいいのでしょうね。

  • 弱い参照WeakReferenceMessengerは購読解除せずとも、ガベージコレクションの対象となります。
  • 強い参照StrongReferenceMessengerは購読解除しないと、メモリリークを起こします。

参考:速度比較

もりゃきの環境で、50000000回単純に呼んだ時のタイムを計測しました。

  • 通常の弱い参照 : 3632ms
  • ラムダ式の弱い参照 : 3215ms
  • ラムダ式の強い参照 : 2964ms

という結果になっています。

リクエスト・メッセージ

メッセージを送信し、何らかの応答が必要な場合は以下のようになります。

今までのようなメッセージの投げっぱなしとは少し毛色が違うため、全コードをきます。

using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;  // RequestMessageを使うために必要!

namespace MessageingTest
{
    /// <summary>
    /// 送信クラス(Main含む)
    /// </summary>
    public static class Sender
    {
        public static void Main()
        {
            var _ = new Receiver();

            // 送信処理
            var message = new MyMessage("もりゃき", 12);
            // 弱い参照でのメッセージング
            var result = WeakReferenceMessenger.Default.Send(message);
            // 応答の出力
            if (result != null)
            {
                Console.WriteLine(result);
            }
            else
            {
                Console.WriteLine("応答がありませんでした。");
            }
            // メッセージが出力されるまで待機
            Console.ReadKey();
        }
    }
    /// <summary>
    /// 受信クラス
    /// </summary>
    public class Receiver
    {
        public Receiver()
        {
            // 弱い参照での受信設定、応答付き
            WeakReferenceMessenger.Default.Register<MyMessage>(this, (_, m) =>
                m.Reply($"{m.Name} の所持金が {m.Value} 円と「3倍」になった。"));
        }
    }

    /// <summary>
    /// メッセージクラス
    /// </summary>
    public class MyMessage : RequestMessage<string> // ただのMyMessageから、RequestMessageを継承してる!
    {
        public string Name { get; set; } = string.Empty;
        public int Value { get; set; }

        public MyMessage() { }

        public MyMessage(string name, int value)
        {
            Name = name;
            Value = value;
        }
    }
}

以下の出力がされますが、今までは受信クラスでの出力だったのに対し、今回は受信クラスから送信クラスへ応答を返して、出力しています。

もりゃき の所持金が 36 円と「3倍」になった。

残高3倍おめでとう、もりゃき!

3倍ご祝儀にStrongReferenceMessenger版も載せましょう!

using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.Mvvm.Messaging.Messages;

namespace MessageingTest
{
    /// <summary>
    /// 送信クラス(Main含む)
    /// </summary>
    public static class Sender
    {
        public static void Main()
        {
            using (var recv = new Receiver())
            {
                // 送信処理
                var message = new MyMessage("もりゃき", 12);
                // 強い参照でのメッセージング
                var result = StrongReferenceMessenger.Default.Send(message);
                // 応答の出力
                if (result != null)
                {
                    Console.WriteLine(result);
                }
                else
                {
                    Console.WriteLine("応答がありませんでした。");
                }
            }
            // メッセージが出力されるまで待機
            Console.ReadKey();
        }
    }
    /// <summary>
    /// 受信クラス
    /// </summary>
    public class Receiver : IDisposable
    {
        public Receiver()
        {
            Console.WriteLine("Register StrongReferenceMessenger");
            // 強い参照での受信設定、応答付き
            StrongReferenceMessenger.Default.Register<MyMessage>(this, (_, m) =>
                m.Reply($"{m.Name} の所持金が {m.Value} 円と「3倍」になった。"));
        }

        private bool isDisposed = false;
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (isDisposed) return;
            if (disposing)
            {
                StrongReferenceMessenger.Default.Unregister<MyMessage>(this);
                Console.WriteLine("Unregister StrongReferenceMessenger");
            }
            isDisposed = true;
        }
    }

    /// <summary>
    /// メッセージクラス
    /// </summary>
    public class MyMessage : RequestMessage<string>
    {
        public string Name { get; set; } = string.Empty;
        public int Value { get; set; }

        public MyMessage() { }

        public MyMessage(string name, int value)
        {
            Name = name;
            Value = value;
        }
    }
}

メッセージングの何が嬉しいのか?

これは Send で「大声を出して伝達」するようなもので、Register している全ての所が聞くようなものです。

今回は送信クラスと受信クラスと、単純化した構造だったのでありがたみは分かりにくかったかもしれません。しかしソースをよく見てください、MainReceiver を生成するだけで、全く使っていないのです。

これは、プロジェクトの規模が大きくなればなるほど、親子関係のクラス(親から子の操作ならいいけど、子から親の操作はとても望ましくない)や関係性が遠いクラスが当然出てきます。

そういう、通常利用で解決が困難な時にメッセージングを使えるならば、比較的容易に問題が解決するというわけです。

もちろん濫用は厳禁です、

前回の宿題の答え一例

「WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き(2) 依存性注入(DI)を利用した簡単なサンプル」 の宿題はやって頂けたでしょうか?簡単にメッセージングを使ったケースの一例を示します。

まずは App.xaml.csConfigureServices を以下のように、メッセンジャーをDI登録します。

private static ServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    // Messenger
    services.AddSingleton<IMessenger, WeakReferenceMessenger>();
    // ViewModel
    services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>();
    // ViewNmae の ViewModel を登録する
    services.AddSingleton<IViewNameViewModel, ViewNameViewModel>();
    return services.BuildServiceProvider();
}

ViewModels\MainWindowViewModel.cs コンストラクタ部分

public MainWindowViewModel()
{
    NameCommand = new RelayCommand(
        () =>
        {
            var viewNameWindow = new Views.ViewName();
            viewNameWindow.Show();
        },
        () => !string.IsNullOrEmpty(_name)
    );
}

を以下のように書き換え、メッセージを送信します。

private readonly IMessenger _messenger;

public MainWindowViewModel(IMessenger messenger)
{
    _messenger = messenger;

    NameCommand = new RelayCommand(
        () => _messenger.Send(new OpenViewNameWindowMessage()),
        () => !string.IsNullOrEmpty(_name)
    );
}

次に、`Views\MainWindow.xaml.cs クラス部分

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = Ioc.Default.GetService<IMainWindowViewModel>();
    }
}

を以下のように書き換え、メッセージを受信します。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = Ioc.Default.GetService<IMainWindowViewModel>();

        // メッセージの受信      
        var messenger = WeakReferenceMessenger.Default;
        messenger.Register<OpenViewNameWindowMessage>(this, (_, _) =>
        {
            var viewNameWindow = new Views.ViewName();
            viewNameWindow.Show();
        });
    }
}

public class OpenViewNameWindowMessage;

これで、WPFを中心としたMVVM論 に書いたように「View は ViewModel を知っているが、ViewModel は View を知らない」を達成できました。

恥ずかしながら、偉そうに書いておきながら見落としてました。

最後に

ここまで全三回に渡って「WPF における CommunityToolkit.Mvvm(MVVM ToolKit) 覚え書き」シリーズを書きましたが、当然ながらこれで全てを網羅している訳ではありません。

特に非同期が関わる所は、自分の未熟さ故に完全に未着手です。

私が見た時には、まだ公式に日本語訳がなかった気もしますが、今は MVVM Toolkitの概要が日本語訳されていますね。

英語版もお好みに応じてどうぞ。

自分にとっては、英語版公式は少しハードルが高かったので、Google検索上位にくるサラッとした解説を読んだりChatGPTに聞いて、色々手を動かし悪戦苦闘して、やっとここまで来ました。

誰か一人でも、ここにたどり着いて「よかった」と思って頂ければ幸いです。