銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

MVVM的に真っ当にMessageBoxを表示する

f:id:takamints:20161023233634p:plain
photo credit: waterj2 DSCF0629 via photopin (license)

MVVM的に正しくモーダルなメッセージボックスを表示するサンプルコードを示します。

WPFでのMVVMパターンとしては、ビュー以外からメッセージボックスを直接表示するのは良くないらしいです。

メッセージボックスで親ウィンドウを指定しないとモーダルにならないのですが「ビュー以外からメインウィンドウを逆方向へ参照するのは悪手」ということです。

じゃあ、どうすればよいかっていうと「メッセンジャーパターン」を使いましょうとのことでした。

メッセンジャーパターン」て?

メッセンジャーパターンは「VMからViewを操作する方法」のことらしいです。 以下のようなカラクリだと理解しました。

  1. メッセンジャー」というオブジェクトからイベントを発行して、
  2. あらかじめビューに実装されたイベントトリガーが、このイベントを受けて、
  3. イベントトリガーが保有するトリガーアクションが処理をする(イベントの引数も渡される)。

上に書いた点を自分なりに整理して、ややこしいことを抜きで System.Windows.MessageBox と同じように使えるコードをご紹介します。 GitHub Gistにも置いていますのでご自由にご利用ください。

コードの概要

  1. メッセンジャーとイベントトリガー(これが本体)
  2. トリガーアクションの実装例
  3. メインウィンドウ(MainWindow.xaml)の記述例
  4. メッセージボックスを表示する

ここで、

  1. メッセンジャーは単なるクラス。
  2. イベントトリガーは、System.Windows.Interactibity.EventTriggerの派生クラス。
  3. トリガーアクションは、System.Windows.Interactibity.TriggerActionの派生クラス。

※ System.Windows.Interactibityは、プロジェクトから参照されていないかもしれません。 参照マネージャの左のツリーから[アセンブリ]/[拡張]を開いてチェックを付ければOKです。

1.メッセンジャーとイベントトリガー(これが本体)

内部クラスを含めて3つのクラスを定義しています。System.Windows.MessageBoxと同じように使えるようにしたら長くなりました。

  1. MessageBox - メッセンジャー。スタティックなShowメソッドでシングルトンのインスタンスからイベントを発行。
  2. MessageBox.Action トリガーアクションの抽象基底クラス。実際にメッセージを表示するクラスの基本クラスです。
  3. MessageBoxTrigger - イベントトリガークラス。MessageBoxからのイベントを受け取って、トリガーアクションを実行します。

MessageBoxTrigger.cs

using System;
using System.Linq;
using System.Windows;
using System.Windows.Interactivity;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// MVVM的メッセージボックスを表示するためのメッセンジャー。
    ///
    /// MessageBox.Showメソッドで、イベントトリガーを起動する。
    /// 
    /// 実際の表示は、イベントトリガーから実行される
    /// トリガーアクションに実装される。
    /// </summary>
    public class MessageBox
    {
        /// <summary>
        /// MessageBoxTriggerを起動するイベント。
        /// </summary>
        public event EventHandler<EventArgs> ShowMessageBox;

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの名前。
        /// </summary>
        public static string EventName
        { get { return "ShowMessageBox"; } }

        /// <summary>
        /// このクラスはシングルトン。
        /// </summary>
        public static MessageBox Instance
        { get; private set; } = new MessageBox();
        private MessageBox() { }

        /// <summary>
        /// MessageBoxTriggerを起動するイベントの引数。
        /// トリガーアクションへ渡されて処理される。
        /// </summary>
        public class EventArgs : System.EventArgs
        {
            public string Text { get; set; }
            public string Title { get; set; }
            public MessageBoxButton Button { get; set; }
            public MessageBoxImage Icon { get; set; }

            /// <summary>
            /// メッセージボックスの結果を受け取るコールバック
            /// </summary>
            public Action<MessageBoxResult> NotifyResult
            { get; set; }
        }

        /// <summary>
        /// MVVM的メッセージボックスを表示。
        /// 実際にはイベントを発行してイベントトリガーを起動する。
        /// </summary>
        /// <param name="messageBoxText"></param>
        /// <param name="title"></param>
        /// <param name="button"></param>
        /// <param name="icon"></param>
        /// <returns></returns>
        public static MessageBoxResult Show(
            string messageBoxText,
            string title = null,
            MessageBoxButton button = MessageBoxButton.OK,
            MessageBoxImage icon = MessageBoxImage.Information)
        {
            //メッセージボックスの結果
            MessageBoxResult messageBoxResult = MessageBoxResult.Cancel;

            //イベントを発行する
            Instance.ShowMessageBox?.Invoke(
                Instance,
                new MessageBox.EventArgs()
                {
                    Text = messageBoxText,
                    Title = title,
                    Button = button,
                    Icon = icon,

                    //コールバックで結果を受け取る
                    NotifyResult = result =>
                    {
                        messageBoxResult = result;
                    }
                });

            //メッセージボックスの結果を返す
            return messageBoxResult;
        }

        /// <summary>
        /// メッセージを表示するトリガーアクション実装用の抽象基底クラス。
        /// 
        /// 派生クラスでShowMessageを実装する。 
        /// </summary>
        public abstract class Action : TriggerAction<DependencyObject>
        {
            /// <summary>
            /// アクションの実態
            /// </summary>
            /// <param name="parameter"></param>
            protected override void Invoke(object parameter)
            {
                //イベント引数の種別を検査
                var messageBoxArgs = parameter as MessageBox.EventArgs;
                if(messageBoxArgs == null)
                {
                    return;
                }

                //メッセージボックスの表示結果を取得
                MessageBoxResult result = ShowMessage(
                        messageBoxArgs.Text,
                        messageBoxArgs.Title,
                        messageBoxArgs.Button,
                        messageBoxArgs.Icon);

                //コールバックで結果を通知
                messageBoxArgs.NotifyResult?.Invoke(result);
            }

            /// <summary>
            /// メッセージボックスを表示する抽象メソッド。
            /// </summary>
            /// <param name="text"></param>
            /// <param name="title"></param>
            /// <param name="button"></param>
            /// <param name="icon"></param>
            /// <returns></returns>
            abstract protected MessageBoxResult
            ShowMessage(
                string text, string title,
                MessageBoxButton button,
                MessageBoxImage icon);
        }
    }

    /// <summary>
    /// MVVM的メッセージボックスを表示するためのイベントトリガー。
    ///
    /// MessageBox メッセンジャーの発行するイベントで起動される。
    /// </summary>
    public class MessageBoxTrigger : System.Windows.Interactivity.EventTrigger
    {
        public MessageBoxTrigger()
            : base(MessageBox.EventName)
        {
            SourceObject = MessageBox.Instance;
        }
    }
}

2.トリガーアクションの実装例

MesssageBoxのトリガーから実行されるトリガーアクションクラスの実装例です。

MessageBox.Actionから派生した、MessageBoxActionクラスを実装しています。 メッセージボックスを表示して、その結果を返すクラスです。

このクラスから使っている MessageBox クラスは、System.Windows のものということに注意してください。

MessageBoxAction.cs

using System.Windows;

namespace StudyDotNet.Triggers
{
    /// <summary>
    /// メッセージボックスを表示するトリガーアクション
    /// </summary>
    public class MessageBoxAction : MessageBox.Action
    {
        protected override MessageBoxResult ShowMessage(
            string text,
            string title,
            MessageBoxButton button,
            MessageBoxImage icon)
        {
            var owner = Application.Current.Windows
                .OfType<Window>().SingleOrDefault(
                    x => x.IsActive);
            if(owner == null)
            {
                owner = Window.GetWindow(AssociatedObject);
            }
            return System.Windows.MessageBox.Show(
                owner, text,
                (title != null ? title :
                    Application.Current.MainWindow.Title),
                button, icon);
        }
    }
}

追記(2017-03-29): メッセージボックスのオーナーウィンドウに、アクティブウィンドウを指定するようにしました。メッセージボックスが出ている場合はnullになりうるのでその場合は従来通りのウィンドウとしています。(c# - Refer to active Window in WPF? - Stack Overflow

3.メインウィンドウ(MainWindow.xaml)の記述例

上のメッセージトリガーとトリガーアクションクラスは以下のように、Xamlに Interaction.Trigger 要素を追加します。

XML名前空間xmlns:ixmlns:tr の宣言にも注意。iは、System.Windows.Interactibityを使用するためで、 trは、上記のトリガー/アクションを使用するためのものです。

<Window x:Class="StudyDotNet.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:StudyDotNet"
        xmlns:vm="clr-namespace:StudyDotNet.ViewModels"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:tr="clr-namespace:StudyDotNet.Triggers"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <!-- ここから -->
    <i:Interaction.Triggers>
        <tr:MessageBoxTrigger>
            <tr:MessageBoxAction />
        </tr:MessageBoxTrigger>
    </i:Interaction.Triggers>
    <!-- ここまで -->

    <Grid>
        <Button x:Name="button" Content="Button"
                Command="{Binding SampleCommand}"
                HorizontalAlignment="Left" Margin="50,50,0,0"
                VerticalAlignment="Top" Width="75"/>
    </Grid>
</Window>

4.メッセージボックスを表示する

使う側からは単純に、MessageBox.Showを呼び出すだけです。 System.Windows.MessageBoxとゴッチャにならないように注意は必要。

using System;
using System.Windows.Input;
using StudyDotNet.Triggers;

namespace StudyDotNet.Commands
{
    public class SampleCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        { return true; }

        public void Execute(object parameter)
        {
            MessageBox.Show("MVVMバンザイ!");
        }
    }
}

いくつになってもお勉強

これによって、より少ない変更で、従来のメッセージボックス使用部分をメッセンジャーパターンに移行できます。

ユニットテストは、固定の応答を返すスタブで対応。

また、別の表示方法に切り替えるのも容易です。

例えば独自のウィンドウで表示したり、 ポップアップせずメインウィンドウ内にメッセージを配置したり、 履歴を参照する機能を追加したりと、広がりがあります。

ということで、やはりビュー以外からは、表示内容やUIに直接の関わりを持たないほうが良いのでしょう。

いくつになってもお勉強です。