MVVM的に真っ当にMessageBoxを表示する
photo credit: waterj2 DSCF0629 via photopin (license)
MVVM的に正しくモーダルなメッセージボックスを表示するサンプルコードを示します。
WPFでのMVVMパターンとしては、ビュー以外からメッセージボックスを直接表示するのは良くないらしいです。
メッセージボックスで親ウィンドウを指定しないとモーダルにならないのですが「ビュー以外からメインウィンドウを逆方向へ参照するのは悪手」ということです。
じゃあ、どうすればよいかっていうと「メッセンジャーパターン」を使いましょうとのことでした。
「メッセンジャーパターン」て?
メッセンジャーパターンは「VMからViewを操作する方法」のことらしいです。 以下のようなカラクリだと理解しました。
- 「メッセンジャー」というオブジェクトからイベントを発行して、
- あらかじめビューに実装されたイベントトリガーが、このイベントを受けて、
- イベントトリガーが保有するトリガーアクションが処理をする(イベントの引数も渡される)。
上に書いた点を自分なりに整理して、ややこしいことを抜きで System.Windows.MessageBox と同じように使えるコードをご紹介します。 GitHub Gistにも置いていますのでご自由にご利用ください。
コードの概要
ここで、
- メッセンジャーは単なるクラス。
- イベントトリガーは、System.Windows.Interactibity.EventTriggerの派生クラス。
- トリガーアクションは、System.Windows.Interactibity.TriggerAction
の派生クラス。
※ System.Windows.Interactibityは、プロジェクトから参照されていないかもしれません。 参照マネージャの左のツリーから[アセンブリ]/[拡張]を開いてチェックを付ければOKです。
1.メッセンジャーとイベントトリガー(これが本体)
内部クラスを含めて3つのクラスを定義しています。System.Windows.MessageBoxと同じように使えるようにしたら長くなりました。
- MessageBox - メッセンジャー。スタティックなShowメソッドでシングルトンのインスタンスからイベントを発行。
- MessageBox.Action トリガーアクションの抽象基底クラス。実際にメッセージを表示するクラスの基本クラスです。
- 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:i
と xmlns: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に直接の関わりを持たないほうが良いのでしょう。
いくつになってもお勉強です。