はじめに
オレオレ解釈の覚え書き その6
今回から ViewModel のお話です。ここでは相互作用処理についてまとめます。
本文
MVVM における相互作用処理とはユーザとの対話が発生する処理を指します。ダイアログを表示して Yes/No の選択を促す場合などがこれにあたります。こういった処理ではユーザの判断を待たなければなりませんが、直接 View にアクセスできない ViewModel はどのように実装すればよいでしょうか?
MVVM を学び始めたころの私は、このパターンで構築するにあたり、なんとしても ViewModel だけで処理を実現させようと躍起になっていました。当時は参考になる文献が少なく、思い通りに実装できない状態が続き、MVVM への苦手意識を持ったポイントにもなりました。
現在は強力なフレームワークがいくつも登場し、相互作用処理を容易に実現できる環境が整っています。Microsoft 製の Prism では InteractionRequest と InteractionRequestTrigger を組み合わせてこれを実現しています。InteractionRequest クラスには Raise メソッドが実装されており、これを実行するとインターフェースで定義された Raised イベントが発生します。このイベントを購読するためのトリガーが InteractionRequestTrigger になります。イベント引数に内包されるオブジェクトにメッセージを格納することで View に情報を伝播させます。
実装例を見てみましょう。ViewModel に InteractionRequest クラスのインスタンスを宣言し、View に InteractionRequestTrigger を配置してイベントを購読します。授受されるオブジェクトのクラスは Prism に用意されている Notification です。アクションには Notification の情報をダイアログにマッピングして表示する処理を実装し、View でトリガーに紐づければ完成です。
namespace TestApp.ViewModels
{
public class MainWindowViewModel
{
public InteractionRequest<Notification> MessageRequest { get; }
= new InteractionRequest<Notification>();
// このメソッドを呼び出せばダイアログを表示できる
public void ShowMessage()
=> this.MessageRequest.Raise(new Notification { Title = "タイトル", Content = "メッセージ" });
}
}
namespace TestApp.Views.Behaviors
{
public class MessageAction : TriggerAction<DependencyObject>
{
protected override void Invoke(object parameter)
{
if (!(parameter is InteractionRequestedEventArgs args))
return;
if (!(args.Context is Notification context))
return;
var owner = this.AssociatedObject as Window ?? Window.GetWindow(this.AssociatedObject);
var message = context.Content?.ToString() ?? string.Empty:
MessageBox.Show(owner, message, context.Title, MessageBoxButton.OK, MessageBoxImage.Information);
}
}
}
<Window x:Class="TestApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:p="http://prismlibrary.com/"
xmlns:behaviors="clr-namespace:TestApp.Views.Behaviors"
p:ViewModelLocator.AutoWireViewModel="True">
<i:Interaction.Triggers>
<p:InteractionRequestTrigger SourceObject="{Binding MessageRequest, Mode=OneTime}">
<behaviors:MessageAction/>
</p:InteractionRequestTrigger>
</i:Interaction.Triggers>
</Window>
上記で ViewModel は間接的に画面を操作していますが、View を直接参照していないため、独立性を保っています。Raised イベントをとらえて結果を返すようなテストロジックを組めば、View を介さずに ViewModel のロジックテストを実施できます。このように仲介者を挟んで双方が通信する方法は Messenger パターンと呼ばれたりもします。
おわりに
InteractionRequest を使った相互作用処理についてまとめました。
今回は Prism での実装でしたが、Livet などほかのフレームワークでは、これらのやり取りをさらにシンプルに実現したものもあります。それぞれ良さがあるので、自分の作り方にあった基盤を取り入れましょう。
実は今回ご紹介した方法は、執筆時点での最新版である Prism 7.2 で非推奨とされました。(使用できないわけではありません。)ただし、この考え方自体は有益であり、汎用性も高く、理解しておいて損はない内容だと思います。次回は新たに採用された相互作用処理の実現方法である DialogService についてまとめます。