WPF Prism episode: 12 ~ Prism メッセージボックスの Service な日常 ~
前回は Prism の IConfirmNavigationRequest インタフェースを利用して Navigation(画面遷移)をキャンセルする方法を紹介しました。
今回は Navigation をキャンセルした際等に Prism 組み込みのメッセージボックスを表示する方法と、Prism 組み込みのメッセージボックス表示処理を Service として分離する方法を紹介します。
2019/7/25 に Prism 7.2 がリリースされ、ダイアログの表示に使用していた INotification、IConfirmation が廃止になり、新たに IDialogService が導入されました。
【WPF Prism episode: 16 ~ Prism7.2、ダイアログは IDialogService でって言ったよね! ~】で使用方法を紹介しています。
尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty + Extended WPF Toolkit™ を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
Prism 組み込みのメッセージボックスを MVVM パターンで表示する
前回のエントリでは Validation でエラーが存在した場合に View の遷移をキャンセルする方法を紹介しましたが、現状の動作では通常の Validation と同じエラーメッセージが表示されるだけなので、何故 View が切り替わらないかユーザに伝わりにくいと思います。
こんな場合は、遷移できない理由をメッセージボックスで表示するのがよくあるパターンだと思うので Prism からメッセージボックスを表示する方法を紹介します。
まず、WPF でメッセージボックスを表示する最も簡単な方法は Windows Form でもお馴染みの MessageBox クラスを使用する方法で、表示する方法も Windows Form の場合と変わりませんが、MVVM パターンで開発する場合は
- 「MessageBox の表示は View の責務なんだから VM から MessageBox を Show するのは気持ち悪い!」
- 「ユニットテスト中に MessageBox が表示されたらそこで止まっちゃうだろ!」
等の意見もある為、忌避される傾向にあるようです。
そのため、MVVM パターンで作成する WPF アプリケーションから MessageBox を表示するためのデザインパターンに【Messenger パターン】が採用される事が多いようですが、Prism は【Messenger パターン】で実装されたメッセージボックスを VM から表示する機能を標準で備えています。
Messenger パターンについては Microsoft MVP の gushwell さんが『MVVM:Messengerを理解するために自作してみた(1)』で紹介されている(2022/8/23 現在、サイトが無くなっているようです)ので、Messenger パターンの内部動作に興味があれば見てみると良いでしょう。
管理人はアプリが作れればいいので内部動作までは確認していませんw
OK ボタンのみのメッセージボックスを MVVM パターンで表示する
Prism でメッセージボックスを表示するためにまず PhysicalEditView.xaml を以下のように変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <UserControl x:Class="WpfTestApp.Views.PhysicalEditor" ~ 略 ~ xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:prism="http://prismlibrary.com/" ~ 略 ~ xmlns:xceed="http://schemas.xceed.com/wpf/xaml/toolkit" prism:ViewModelLocator.AutoWireViewModel="True"> ~ 略 ~ <i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding MessageBoxRequest}"> <prism:PopupWindowAction IsModal="True" WindowStartupLocation="CenterOwner" /> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> ~ 略 ~ </UserControl> |
episode:6 でも紹介した Interaction.Triggers を使用するために System.Windows.Interactivity.dll のエイリアスを追加します。(3 行目)
続いて、VM とバインドするための InteractionRequestTrigger を定義して、実行するアクションに Prism の PopupWindowAction を指定して、VM とのバインド先を SourceObject に設定します。
以下のプロパティも併せて設定して XAML 側の変更は完了です。
プロパティ | 説明 |
---|---|
IsModal | true を指定すると PopupWindow をモーダルで表示します。 |
CenterOverAssociatedObject | PopupWindow を TriggerAction を配置したコントロールの中央に表示します。 ここでは UserControl に配置しているので、UserControl(身体測定データ編集ビュー)の中央に表示されます。 ※ WindowStartupLocation プロパティと同時に設定すると無視されるようです |
WindowStartupLocation | TriggerAction を UserControl に配置した場合でも UserControl のオーナー(Window)中央にメッセージボックスを表示します。 ※ アプリ画面の中央に表示する場合はこちらのプロパティのみ設定すれば OK です |
次に VM 側へ以下のハイライト行を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | using System.ComponentModel.DataAnnotations; using Prism.Mvvm; using Prism.Regions; using Prism.Interactivity.InteractionRequest; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace WpfTestApp.ViewModels { /// <summary> 身体測定データの編集画面を表します。 </summary> public class PhysicalEditorViewModel : BindableBase, IDisposable, IConfirmNavigationRequest { ~ 略 ~ /// <summary>情報MessageBoxを表示します。</summary> public InteractionRequest<INotification> MessageBoxRequest { get; private set; } /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> private void showInformationMessage(string message, string title = "情報") { var notify = new Notification() { Content = message, Title = title }; this.MessageBoxRequest.Raise(notify); } ~ 略 ~ /// <summary>他 View への遷移を確認します。</summary> /// <param name="navigationContext">遷移先の情報を表すNavigationContext。</param> /// <param name="continuationCallback">遷移を続行するかを表すコールバック。</param> void IConfirmNavigationRequest.ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) { this.MeasurementDate.ForceValidate(); this.Height.ForceValidate(); this.Weight.ForceValidate(); var isMove = !(this.MeasurementDate.HasErrors | this.Height.HasErrors | this.Weight.HasErrors); continuationCallback(isMove); if (!isMove) this.showInformationMessage("エラーが存在するため、別画面を表示できません。"); } /// <summary>測定日のエラー文字列を取得します。</summary> /// <param name="value">View で入力されたDateTime?。</param> /// <returns>測定日のエラー文字列</returns> private string getMeasurementDateError(DateTime? value) { if (!value.HasValue) return "必須入力です。"; if (this.appData.HasPhysicalKey(value, this.physical)) { this.MeasurementDate.Value = this.physical.MeasurementDate; return "既に同一の測定日が存在するため、別の日付を設定してください。"; } else return null; } ~ 略 ~ /// <summary>コンストラクタ。</summary> /// <param name="data">アプリのデータオブジェクト(Unity からインジェクション)</param> public PhysicalEditorViewModel(WpfTestAppData data) { this.appData = data; this.MessageBoxRequest = new InteractionRequest<INotification>(); } ~ 略 ~ } } |
まず、XAML で定義した InteractionRequestTrigger とバインドするための InteractionRequest を定義して、コンストラクタ等で初期化します。(15、70 行目。初期化する場所はどこでも構いません)
実際にメッセージボックスを表示するための処理は MessageBox クラスの ShowDialog メソッドを模した showInformationMessage メソッドに切り出しておきます。(20 ~ 29 行目)
※ メソッドの切り出しは必須ではありませんが、切り出した方が使い易いと思います。
メッセージボックスのタイトルを毎回指定するのは面倒なので、showInformationMessage メソッドの第 2 パラメータ: title は、省略可能パラメータとして定義していますが、どちらでも構いません。
showInformationMessage メソッド内で宣言している Prism.Interactivity.InteractionRequest.Notification は Prism に標準で組み込まれているOK ボタンのみを備えたダイアログを表します。
そしてメッセージボックスを呼び出したい場所から showInformationMessage メソッドを呼び出す(44 – 45 行目)と、以下のように Prism 組み込みの OK ボタンのみのメッセージボックスが表示されます。
Windows Form でメッセージボックスを表示するコードと比べてかなり記述量が増えますが、MVVM パターン で開発する際、まず最初にぶつかる壁と言う話もよく聞くメッセージボックスの表示を、比較的簡単に呼び出す仕組みが標準で備わっているのは Prism の利点と言えると思います。
ただ、このメッセージボックス、手軽に使えるのは良いのですが、下 fig.2 の赤枠で囲んだ部分のようにタスクバーに別 Window として表示されてしまいます。
オブジェクトブラウザで Prism.Interactivity.PopupWindowAction を見ても ShowInTaskBar プロパティが無いので諦めようかと思いましたが、ググると Stack Overflow に解決法がありました。
PopupWindowAction へ Style を定義すると対応できるようなので XAML へ以下の Style を追加。
1 2 3 4 5 6 7 8 9 10 11 12 | <i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding InformationMessage}"> <prism:PopupWindowAction IsModal="True" WindowStartupLocation="CenterOwner"> <prism:PopupWindowAction.WindowStyle> <Style TargetType="Window"> <Setter Property="ShowInTaskbar" Value="False" /> </Style> </prism:PopupWindowAction.WindowStyle> </prism:PopupWindowAction> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> |
実行して確認すると…
確かにタスクバー上にメッセージボックスは表示されなくなりましたが、予想外に巨大なメッセージボックスが表示されて一瞬ビビりましたが、以下の Style を再度追加するとちゃんとした大きさのメッセージボックスが表示されるようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding InformationMessage}"> <prism:PopupWindowAction IsModal="True" WindowStartupLocation="CenterOwner"> <prism:PopupWindowAction.WindowStyle> <Style TargetType="Window"> <Setter Property="ShowInTaskbar" Value="False" /> <Setter Property="SizeToContent" Value="WidthAndHeight" /> </Style> </prism:PopupWindowAction.WindowStyle> </prism:PopupWindowAction> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> |
ここまで設定してやっと以下のように思った通りのメッセージボックスが表示されるようになります。
OK とキャンセルボタンのあるメッセージボックスを MVVM パターンで表示する
Prism 組み込みのメッセージボックスは情報を通知する OK ボタンのみのメッセージボックスだけではなく、OK・キャンセルボタンが配置された処理の続行を確認するためのメッセージボックスも組み込まれています。
確認メッセージボックスを表示する方法も 情報メッセージボックスの場合とほとんど同じで、XAML へは以下のように定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <UserControl x:Class="WpfTestApp.Views.PhysicalEditor" ~ 略 ~ xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:prism="http://prismlibrary.com/" ~ 略 ~ xmlns:xceed="http://schemas.xceed.com/wpf/xaml/toolkit" prism:ViewModelLocator.AutoWireViewModel="True"> ~ 略 ~ <i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding MessageBoxRequest}"> <prism:PopupWindowAction IsModal="True" WindowStartupLocation="CenterOwner" /> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> ~ 略 ~ </UserControl> |
実は、確認メッセージボックスを表示するための XAML 側の記述は Notification の場合と全く同じで、違うのは SourceObject に設定するバインド先のみです。
Prism の公式サンプル等でも別々に定義されているため、別々に定義する必要があるように感じますが、XAML 側の定義は Notification と共有できるので、1 つの画面で両方のメッセージボックスを表示する場合でも定義は 1 つだけで動作します。
又、共有できるのは XAML 側の定義だけではなく VM 側のインタフェースも同様です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | using System.Windows; using Prism.Mvvm; using Prism.Regions; using Prism.Interactivity.InteractionRequest; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace WpfTestApp.ViewModels { /// <summary> 身体測定データの編集画面を表します。 </summary> public class PhysicalEditorViewModel : BindableBase, IDisposable, IConfirmNavigationRequest { ~ 略 ~ /// <summary>情報MessageBoxを表示します。</summary> public InteractionRequest<INotification> MessageBoxRequest { get; private set; } /// <summary>確認メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> /// <returns>メッセージボックスの選択結果を表すMessageBoxResult列挙型の内の1つ。</returns> private MessageBoxResult showConfirmMessage(string message, string title = "問い合わせ") { var confirm = new Confirmation() { Content = message, Title = title }; var msgRet = MessageBoxResult.Cancel; this.MessageBoxRequest.Raise(confirm, r => { msgRet = (r as IConfirmation).Confirmed ? MessageBoxResult.OK : MessageBoxResult.Cancel; }); return msgRet; } /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> private void showInformationMessage(string message, string title = "情報") { var notify = new Notification() { Content = message, Title = title }; this.MessageBoxRequest.Raise(notify); } /// <summary>他 View への遷移を確認します。</summary> /// <param name="navigationContext">遷移先の情報を表すNavigationContext。</param> /// <param name="continuationCallback">遷移を続行するかを判定するコールバック。</param> public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) { this.MeasurementDate.ForceValidate(); this.Height.ForceValidate(); this.Weight.ForceValidate(); var isMove = true; if (this.MeasurementDate.HasErrors | this.Height.HasErrors | this.Weight.HasErrors) { if (this.showConfirmMessage("別画面に遷移しますか?") == MessageBoxResult.Cancel) isMove = false; } continuationCallback(isMove); } ~ 略 ~ /// <summary>アプリデータ本体を表します。</summary> private WpfTestAppData appData = null; /// <summary>コンストラクタ。</summary> /// <param name="data">アプリのデータオブジェクト(Unity からインジェクション)</param> public PhysicalEditorViewModel(WpfTestAppData data) { this.appData = data; this.MessageBoxRequest = new InteractionRequest<INotification>(); } ~ 略 ~ } } |
15 行目で宣言している InteractionRequest<INotification> MessageBoxRequest プロパティは Notification で情報メッセージボックスを表示した時から変更していないため、情報・確認メッセージボックスを呼び出すインタフェースとして両方から共有しています。
確認メッセージボックスを実際に表示するための showConfirmMessage メソッドの内部では、Prism.Interactivity.InteractionRequest.Confirmation を new して InteractionRequest の Raise メソッドにセットしています。(30 ~ 33 行目)
Notification と Confirmation で InteractionRequest を共有できるのは、両クラスが下 fig.5 のクラス図ような継承関係にあるからです。
Notification と違い、Confirmation はユーザが選択したボタンを取得する必要があるため、Raise メソッドには戻り値を取得するためのコールバックが指定できる overload が 定義されています。
overload で定義されている第 3 パラメータ: callback に返される IConfirmation.Confirmed プロパティに true がセットされている場合は OK ボタン。false がセットされている場合はキャンセルボタンが押されたことが判別できます。
実行すると、以下のような問い合わせメッセージボックスが表示されます。
キャンセルをクリックすると画面遷移がキャンセルされ、OK をクリックすると TreeView で選択した View に切り替わる動作が確認できます。
独り言 実際のアプリでは、この場面で問い合わせメッセージを表示するのはおかしいと思いますが、組み込む場所が思い付かなかったので、あくまでサンプルとして見てください。
メッセージボックス表示機能を使い回すために Service として分離する
Prism 組み込みのメッセージボックスを表示する方法は前章で紹介した通りですが、他の View でもメッセージボックスを表示したい場合、View ごとに同じ記述を追加するのは非常に非効率です。
Prism で作成するアプリは 1 つの Window と複数の View(UserControl)で構成されるようなパターンが多いので、メッセージボックスの表示は Window に任せて他の View は Window にバインドされたメッセージボックスを呼び出せる仕組みにできれば開発効率が良さそうです。
このような場合はメッセージボックス表示機能を Service として分離すると他 View でも使い回すことができます。
Prism を使用せず WPF 標準の機能のみで Service に分離する方法が SourceChord の『MVVMな設計のTips~サービスを作ってVMの依存性を排除~』 で紹介されているので、この方法を Prism で作成するアプリに応用します。
まず、【メッセージボックスを表示する Service のインタフェース】を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | using System.Windows; using Prism.Interactivity.InteractionRequest; namespace WpfTestApp { public interface IMessageBoxService { /// <summary>MessageBoxを表示します。</summary> InteractionRequest<INotification> MessageBoxRequest { get; } /// <summary>確認メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> /// <returns>メッセージボックスの選択結果を表すMessageBoxResult列挙型の内の1つ。</returns> MessageBoxResult ShowConfirmMessage(string message, string title); /// <summary>確認メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <returns>メッセージボックスの選択結果を表すMessageBoxResult列挙型の内の1つ。</returns> MessageBoxResult ShowConfirmMessage(string message); /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> void ShowInformationMessage(string message, string title); /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> void ShowInformationMessage(string message); } } |
IMessageBoxService の実装クラスも追加します。実装の中身は前章で View に実装した内容と同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | using System.Windows; using Prism.Interactivity.InteractionRequest; namespace WpfTestApp { public class MessageBoxService : IMessageBoxService { /// <summary>MessageBoxを表示します。</summary> public InteractionRequest<INotification> MessageBoxRequest { get; } /// <summary>確認メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> /// <returns>メッセージボックスの選択結果を表すMessageBoxResult列挙型の内の1つ。</returns> public MessageBoxResult ShowConfirmMessage(string message, string title) { var confirm = new Confirmation() { Content = message, Title = title }; var msgRet = MessageBoxResult.Cancel; this.MessageBoxRequest.Raise(confirm, r => { msgRet = (r as IConfirmation).Confirmed ? MessageBoxResult.OK : MessageBoxResult.Cancel; }); return msgRet; } /// <summary>確認メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <returns>メッセージボックスの選択結果を表すMessageBoxResult列挙型の内の1つ。</returns> public MessageBoxResult ShowConfirmMessage(string message) { return this.ShowConfirmMessage(message, "確認"); } /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> /// <param name="title">メッセージボックスのタイトルを表す文字列。</param> public void ShowInformationMessage(string message, string title) { var notify = new Notification() { Content = message, Title = title }; this.MessageBoxRequest.Raise(notify); } /// <summary>情報メッセージボックスを表示します。</summary> /// <param name="message">メッセージボックスに表示する内容を表す文字列。</param> public void ShowInformationMessage(string message) { this.ShowInformationMessage(message, "情報"); } /// <summary>コンストラクタ。</summary> public MessageBoxService() { this.MessageBoxRequest = new InteractionRequest<INotification>(); } } } |
ここではちゃんとメッセージボックスを表示するクラスを紹介していますが、このクラスとは別に表示する文字列を Debug.WriteLine するようなテストクラスを作成しておくとユニットテストでメッセージボックスが表示されずに済むでしょう。
DI コンテナの Unity へ登録したインタフェースを Singleton として扱う
作成した Service をサンプルアプリで使用するために、App.xaml のコードビハインドで override している RegisterTypes メソッドで Service を DI コンテナの Unity へ登録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using System.Windows; using Prism.Ioc; using Prism.Modularity; using WpfTestApp.Views; namespace WpfTestApp { /// <summary> /// Interaction logic for App.xaml /// </summary> public partial class App { ~ 略 ~ protected override void RegisterTypes(IContainerRegistry containerRegistry) { containerRegistry.RegisterInstance<WpfTestAppData>(DataLoader.Load(this.dataFilePath)); containerRegistry.RegisterSingleton<IMessageBoxService, MessageBoxService>(); } } } |
IMessageBoxService インタフェースの使い方自体は後述しますが、このインタフェースは Window と View(UserControl)間でインスタンスを共有する必要があるため、Singleton である必要があります。
16 行目の RegisterInstance は episode: 4 で紹介したデータクラスのインスタンスを登録するためのメソッドですが、今回作成した IMessageBoxService インタフェースは 17 行目の RegisterSingleton メソッドでDI コンテナへ登録しています。
2020/8/8 追記
DI コンテナについては改めて .NET Core WPF Prism MVVM 入門 2020 step: 4 で詳しく紹介しています。
今まで DI コンテナへの登録はインスタンスを登録するための RegisterInstance メソッドしか紹介していませんでしたが、型(インタフェース・クラス)を登録するためのメソッドとして以下の 2 種類が定義されています。
※ Prism の Navigation(画面遷移)に使用する View を登録するための RegisterForNavigation メソッドは除く
- Register
- RegisterSingleton
上記 2 つのメソッドとも登録された型がインジェクションされる場合、DI コンテナ内で生成した新規インスタンスをインジェクションしますが、RegisterSingleton メソッドの型第 2 パラメータへクラスを指定した場合は、第 2 パラメータに指定したクラスのインスタンスを Singleton クラスとしてインジェクションしてくれます。
Service を経由してメッセージボックスを表示する
前章までは Prism の View である UserControl に PopupWindowAction を設定していましたが、ここではメッセージボックスを MainWindow からポップアップするので、MainWindow.xaml へ以下のように PopupWindowAction を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <Window x:Class="WpfTestApp.Views.MainWindow" ~ 略 ~ WindowStartupLocation="CenterScreen" IsTabStop="False"> <i:Interaction.Triggers> <prism:InteractionRequestTrigger SourceObject="{Binding MessageBoxRequest}"> <prism:PopupWindowAction IsModal="True" WindowStartupLocation="CenterOwner"> <prism:PopupWindowAction.WindowStyle> <Style TargetType="Window"> <Setter Property="ShowInTaskbar" Value="False" /> <Setter Property="SizeToContent" Value="WidthAndHeight" /> </Style> </prism:PopupWindowAction.WindowStyle> </prism:PopupWindowAction> </prism:InteractionRequestTrigger> </i:Interaction.Triggers> ~ 略 ~ </Window> |
前章で身体測定データ View に追加したコードと全く同じです。
そして、前章と同じく MainWindow の VM にも InteractionRequest を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using Prism.Mvvm; using Prism.Interactivity.InteractionRequest; namespace WpfTestApp.ViewModels { public class MainWindowViewModel : BindableBase { ~ 略 ~ /// <summary>メッセージボックス表示Messanger</summary> public InteractionRequest<INotification> MessageBoxRequest { get; } /// <summary>コンストラクタ。</summary> /// <param name="msgService">IMessageBoxService。(Unityからインジェクション)</param> public MainWindowViewModel(IMessageBoxServicemsgService) { this.MessageBoxRequest = msgService.MessageBoxRequest; } } } |
定義した InteractionRequest は new せず、コンストラクタにインジェクションされたIMessageBoxService から InteractionRequest を受け取ってセットしています。
(Service 内に定義した InteractionRequest が MainWindow.xaml にバインドされる形になります)
そして、実際にメッセージボックスを表示する箇所は以下のように変更します。(ここでは身体測定データ編集 View)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | using System.ComponentModel.DataAnnotations; using System.Windows; using Prism.Mvvm; using Prism.Regions; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace WpfTestApp.ViewModels { /// <summary> 身体測定データの編集画面を表します。 </summary> public class PhysicalEditorViewModel : BindableBase, IDisposable, IConfirmNavigationRequest { ~ 略 ~ /// <summary>他 View への遷移を確認します。</summary> /// <param name="navigationContext">遷移先の情報を表すNavigationContext。</param> /// <param name="continuationCallback">遷移を続行するかを判定するコールバック。</param> public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback) { this.MeasurementDate.ForceValidate(); this.Height.ForceValidate(); this.Weight.ForceValidate(); //var isMove = !(this.MeasurementDate.HasErrors | this.Height.HasErrors | this.Weight.HasErrors); //continuationCallback(isMove); //if (!isMove) // this.messageBoxService.ShowInformationMessage("エラーが存在するため、別画面を表示できません。"); var isMove = true; if (this.MeasurementDate.HasErrors | this.Height.HasErrors | this.Weight.HasErrors) { if (this.messageBoxService.ShowConfirmMessage("別画面に遷移しますか?") == MessageBoxResult.Cancel) isMove = false; } continuationCallback(isMove); } ~ 略 ~ /// <summary>アプリデータ本体を表します。</summary> private WpfTestAppData appData = null; /// <summary>メッセージボックス表示Service。</summary> private IMessageBoxService messageBoxService = null; /// <summary>コンストラクタ。</summary> /// <param name="data">アプリのデータオブジェクト。(Unity からインジェクション)</param> /// <param name="msgService">メッセージボックス表示インタフェース。(Unity からインジェクション)</param> public PhysicalEditorViewModel(WpfTestAppData data, IMessageBoxService msgService) { this.appData = data; this.messageBoxService = msgService; } ~ 略 ~ } } |
前章では View 内に定義していた InteractionRequest を呼び出していましたが、Service のメソッドを呼び出すように修正しています。IMessageBoxService のインスタンスは MainWindow と同じくコンストラクタへ Unity からインジェクションしてもらっています。
MainWindow と Prism Module 内の View(UserControl)で Service のインスタンスを共有したかったので、DI コンテナへ Service を登録する際に Singleton で登録しました。
後はメッセージボックスを表示したいところで、MessageBoxService のメソッドを呼び出す(33 行目)だけで前章までと変わらず以下のようにメッセージボックスが表示されるはずです。
episode:11 で紹介した ConfirmNavigationRequest 内で情報メッセージを表示する部分はコメントアウトしているので、コメントを切替えると情報・確認メッセージボックスの両方の表示が確認できます。
ここでは MessageBoxService をサンプルアプリのソリューションに含めていますが、別プロジェクトに分けておくと、当然別のプロジェクトへも使い回しができるようになります。
VM がいろんなクラスに依存してきて肥大した場合や、処理の再利用が必要になった場合は、このように Service として分割すると応用が利くでしょうし、ここで紹介した方法を応用すれば、MainWindow でバインドした内容を Service 経由で View へ通知したり、その逆もできると思います。
今回はここで終了です。
次回は Prism からダイアログ(別ウィンドウ)を表示する方法を紹介する予定です。
そしていつものように今回紹介したコードも GitHub リポジトリへ置いています。
次回記事「カスタマイズしたらメッセージボックスだった件【episode: 13 WPF Prism】」
こちらに書かれている内容を参考にメッセージボックスの表示をService化してみました。
私が作っているアプリケーションでは、メイン画面とサブ画面(モードレス:メイン画面が起動時に作成)という構成で、
メイン画面を閉じる際にアプリケーションを終了するかどうかの確認メッセージを表示させるようにしています。
ところが、
メイン画面を閉じる→確認メッセージでOK→終了
となるはずが、
メイン画面を閉じる→確認メッセージでOK→サブ画面で確認メッセージが表示される
という動きになってしまいます。
これは実装がマズいのでしょうか?
返信が遅くなりました。管理人です。
モードレスダイアログは WPF では作ったことが無いこととソースコードも見ていないので想像でしかお答えできませんのでご了承ください。
サブ画面で確認メッセージが表示されるのは、サブ画面に表示する View 側にも InteractionRequestTrigger と PopupWindowAction を設定されているのが原因だと想像しています。
このエントリで紹介している Service はメイン画面のみに InteractionRequestTrigger と PopupWindowAction を設定して、InteractionRequest をサービスで使い回すための方法なので、サブ画面側の XAML に InteractionRequestTrigger と PopupWindowAction を追加しなくても Service 経由でメッセージが表示できるはずです。(モードレスは動かしたことが無いのであくまでも想像ですが…)
ソース(部分的にでも)を GitHub 等で見れれば何か分かるかもしれませんが、今の状態ではこの程度の回答が精一杯です。
回答ありがとうございます。
ご指摘の通り、サブ画面側にもInteractionRequestTriggerとPopupWindowActionを設定していました。
それらを無くせば期待通りの動作になりました。
ありがとうございました。