WPF(MVVM)でMessenger登録後にダイアログが何回も表示された

WPF

事象

ダイアログを開くメッセージを送信したら同じダイアログが何回も表示された。

自作のメッセンジャー

    public static class Messenger
    {
        private static readonly Dictionary<MessageType, List<Action<Message>>> _handlers
            = new();

        public static void Register(MessageType type, Action<Message> action)
        {
            if (!_handlers.ContainsKey(type))
                _handlers[type] = new List<Action<Message>>();

            _handlers[type].Add(action);
        }

        public static void Send(Message message)
        {
            if (_handlers.TryGetValue(message.Type, out var actions))
            {
                foreach (var action in actions)
                    action(message);
            }
        }
    }

送信側(ViewModel)ではWeakReferenceMessenger.Default.Sendで送信
※よくわからずWeakReferenceMessengerを使ってた

受信側(View)

      public ViewBase() 
        {
            //viewを呼び出したら受信の設定をする
            Loaded += OnLoaded;
            //閉じたら受信の設定を削除する
            Unloaded += OnUnloaded;
        }

        // 受信の設定をする
        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            if (DataContext == null)
            {
                // ViewModelのインスタンスを作成
                DataContext = ViewModelLocator.Resolve(this);
            }
            RegisterMessages();
        }

        // 受信の設定を削除する
        private void OnUnloaded(object sender, RoutedEventArgs e)
        {
            WeakReferenceMessenger.Default.UnregisterAll(this);
        }

起動して最初にダイアログを出すときは1回表示されて、
別のダイアログを出そうとすると、そのダイアログが2回表示された。

同じ要領で3回4回って同じダイアログが繰り返し表示されるようになっていった

理由

2つあった

自作メッセンジャーの受信の設定が削除できていなかった

渡したいパラメータがいっぱいあったからメッセンジャーを自作していて、
それ自体は問題なく動いてたんだけど、

受信の設定を削除するための
WeakReferenceMessenger.Default.UnregisterAll(this);
↑この記述は自作のメッセンジャー相手には効いてなかった。

新しくviewが呼ばれる度にどんどん受信の設定が登録だけされていく状態

ターゲットを指定していなかった

Viewでメッセージを受信したときに、
「メッセージのパラメータに○○が含まれてたら△△の画面を表示する」
みたいな分岐をさせるべきだった
のに、
させてなかった。

つまり全部のViewに対して受信の設定が登録されていて、
関係ないViewからもダイアログが呼び出されていた

送信も受信もWeakReferenceMessengerを使う

まず自作のメッセンジャーは削除した。

せっかくCommunityToolkit.MVVMを使ってるから
WeakReferenceMessengerをちゃんと使う方針にシフトしたよ。

送信側(ViewModel)

どのViewModelにも継承させる基底クラスで
各メッセージの種類ごとに専用のメッセージ送信メソッドを作った
↓これは警告ダイアログとか確認ダイアログ用の送信メソッド

/// <summary>
/// ポップアップを出すときのメッセージ送信
/// </summary>
/// <param name="target">対象のViewModel</param>
/// <param name="text">本文</param>
/// <param name="title">タイトル</param>
/// <param name="button">ボタン</param>
/// <param name="icon">アイコン</param>
/// <returns>どのボタンを押したか</returns>
protected MessageBoxResult SendPopupMessage(string target, string text, string title = "注意", MessageBoxButton button = MessageBoxButton.OK,
    MessageBoxImage icon = MessageBoxImage.Information)
{
    PopupMessage message = new()
    {
        Target = target,
        Title = title,
        Text = text,
        Button = button,
        Icon = icon,
    };
    WeakReferenceMessenger.Default.Send(message);

    return message.Result;
}

実際に処理の中でダイアログを出したいときにはこんな記述

var result = SendPopupMessage(nameof(xxxxxViewModel)
                              , $"本文"
                              , button: MessageBoxButton.YesNo
                              , icon: MessageBoxImage.Question);

受信側(View)

どのxxxView.csにも継承させるクラスで
画面を開いたら受信の設定を登録して、
画面を閉じたら受信の設定を削除する処理を記述した。

public class ViewBase : Window
{
    public ViewBase() 
    {
        Loaded += OnLoaded;
        Closed += OnClosed;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        if (DataContext == null)
        {
            // ViewModelのインスタンスを作成
            DataContext = ViewModelLocator.Resolve(this);
        }
        RegisterMessages();
    }

    private void OnClosed(object? sender, EventArgs e)
    {
        WeakReferenceMessenger.Default.UnregisterAll(this);
    }

    // 継承先でオーバーロードして実装
    protected virtual void RegisterMessages()
    {
    }
}

これを継承してるxxxView.csでは
RegisterMessages()のオーバーロードをして受信した後の処理を記述した
それと、
if (msg.Target == nameof(xxxxxViewModel))で
View自身に関係あるViewModelからのメッセージなのかどうかを判定するようにした。

 protected override void RegisterMessages()
 {
     // 閉じる
     WeakReferenceMessenger.Default.Register<WindowCloseMessage>(
         this,
         (r, msg) =>
         {
             if (msg.Target == nameof(xxxxxViewModel))
             {
                 Close();
             }
         });

     // 確認、エラーのポップアップを表示
     WeakReferenceMessenger.Default.Register<PopupMessage>(
         this,
         (r, msg) =>
         {
             if (msg.Target == nameof(xxxxxViewModel))
             {
                 msg.Result = MessageBox.Show(
                     msg.Text,
                     msg.Title,
                     msg.Button,
                     msg.Icon
                 );
             }
         });


 }

まとめ

  • Register()は受信処理を登録するメソッド
  • 同じRegister()を何度も呼ぶと、その数だけ処理が実行される
  • 画面を閉じる際にはUnregisterAll()で登録解除する
  • WeakReferenceMessengerを使用すると管理しやすい
  • 「Registerした回数=実行回数」になる
タイトルとURLをコピーしました