📝

WPF MVVMでViewからMessageBoxを呼ぶ

2023/05/23に公開

きっかけ(飛ばしてよし)

JetBrains Rider上でメインウィンドウのxamlファイルを弄るとなぜかMessageBoxが発生した。アプリ起動直前に表示しているMessageBoxと同一であり、アプリを起動していなくても発生して困った…

理由

おそらくRider上でのプレビューあたりの機能がViewModelコンストラクタのMessageBoxを起動していたため、ファイル変更時に再実行され…の繰り返しでメッセージボックスが大量発生していた。

結論

MVVMの思想上ViewModelからViewを操作するのは良くないので、おとなしくView側からMessageBoxを呼ぼう(これでRiderで開発中に実行されることはない)
以下本題

WPF MVVMでViewからMessageBoxを呼ぶ方法

StackOverflowにめちゃくちゃ良い回答があったので要参照
How to show MessageBox in MVVM - StackOverflow

null表記がない等古い記載があった&MessageBoxの第一引数をメッセージ内容に変更して使いやすくしたのを.NET 7で修正したのが以下

ViewModelで使い回せるようにViewModelBaseにView側から登録するevent関数を定義
ViewModel内でMessageBoxをよぶと、View側から登録したコールバックで実行される
MvvmMessageBoxEventArgsは下記にあり

ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
    ...
    
    // ViewModel側からViewへお願いするためのコールバック
    public event EventHandler<MvvmMessageBoxEventArgs>? MessageBoxRequest;
    protected void MessageBox_Show(string messageBoxText, Action<MessageBoxResult>? resultAction = null, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None, MessageBoxOptions options = MessageBoxOptions.None)
    {
        this.MessageBoxRequest?.Invoke(this, new MvvmMessageBoxEventArgs(resultAction, messageBoxText, caption, button, icon, defaultResult, options));
    }
    
    ...
}

以下はView側からで実行されるコールバック上でMessageBoxを呼ぶためのクラス

MvvmMessageBoxEventArgs.cs
// View側から呼ぶためのMessageBoxクラス
public class MvvmMessageBoxEventArgs : EventArgs
{
    public MvvmMessageBoxEventArgs(Action<MessageBoxResult>? resultAction, string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.None, MessageBoxOptions options = MessageBoxOptions.None)
    {
        _resultAction = resultAction;
        _messageBoxText = messageBoxText;
        _caption = caption;
        _button = button;
        _icon = icon;
        _defaultResult = defaultResult;
        _options = options;
    }

    private readonly Action<MessageBoxResult>? _resultAction;
    private readonly string _messageBoxText;
    private readonly string _caption;
    private readonly MessageBoxButton _button;
    private readonly MessageBoxImage _icon;
    private readonly MessageBoxResult _defaultResult;
    private readonly MessageBoxOptions _options;

    public void Show(Window owner)
    {
        var messageBoxResult = MessageBox.Show(owner, _messageBoxText, _caption, _button, _icon, _defaultResult, _options);
        _resultAction?.Invoke(messageBoxResult);
    }

    public void Show()
    {
        var messageBoxResult = MessageBox.Show(_messageBoxText, _caption, _button, _icon, _defaultResult, _options);
        _resultAction?.Invoke(messageBoxResult);
    }
}

最後にView側のビハインドからコールバックを登録すればok
(ViewModelはxml側で指定済み)

MainWindow.xaml.cs
public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            if (DataContext is MainViewModel viewModel)
            {
                viewModel.MessageBoxRequest += MessageBox_Show;
            }
        }

        private void MessageBox_Show(object? sender, MvvmMessageBoxEventArgs args)
        {
            args.Show();
        }
    }

呼ぶときはViewModel上で
MessageBox_Show("メッセージ");
などで標準のメッセージボックス同様に表示できる
当たり前だがコールバックが登録されていない場合は警告もなく表示されないだけなので注意(心配なら処理を追加するなどいいかも)

Discussion