Messenger¶
Messenger パターン¶
一般に WPF/MVVM フレームワークには Messenger と呼ばれる機能が必要となります。
MVVMパターンではView層がViewModel層を参照しますが、逆向きの依存関係はありません。 つまりViewModel層からView層を見ることは出来ません:
View -----> ViewModel
では、View に定義したダイアログを ViewModel から起動したい場合はどうすれば良いのでしょうか。 依存関係がありませんから、ダイアログを ViewModel から直接起動することは出来ません。
この問題を解決するのが Messenger パターンです。 ダイアログを直接起動するのではなく、Messenger を経由して間接的にダイアログを起動するのです。
- ViewModel は Messenger に「Xxxという名前のダイアログを起動して下さい」というメッセージを送ります。
- Messenger はそのメッセージをView層に送ります。
- Viewに設定されたハンドラーがそのメッセージを解釈し、ダイアログを起動します。
MoNo.RAIL の Messenger 機能¶
MoNo.RAIL が備える Messenger 機構は次のような構成になっています。
MoNo.RAIL の MVVM 機構は、ビュー側のコードビハインドを完全にゼロにするフルMVVMではありません。
上図の CustomMessageHandler
の部分はビュー側に C# で記述することになります。
ViewModelクラスは MessengerContext
の派生クラスとして定義します。
MessengerContext
が Messenger
を保持しており、ViewModel はこの Messenger
にメッセージを送ってビュー側と通信します。
Viewは XAML 中に MessengerReceiver
を埋め込み、Messenger
プロパティを ViewModel の Messenger
にバインドします。
これによってViewとViewModelがMessenger機構によって接続され、通信できる状態となります。
MessageReceiver
がメッセージを受け取ると、MessageType
がメッセージの型と合う IMessageHandler
を探索し、メッセージの処理を委譲します。
IMessageHandler
は MEF の仕組みによってプラグインすることが出来ます。
MEFの作法に則り、[Export(typeof(MoNo.Wpf.IMessageHandler))]
という属性を付けて CustomMessageHandler
を定義すると、
MessageReceiver
の探索対象として自動的にプラグインされます。
組み込み済みのメッセージについて¶
頻繁に使う機能についてはメッセージの送受信機能が組み込まれていて、アプリケーション側で Message や MessageHandler を定義しなくても簡単に利用できます。
- 単純なメッセージボックスは
ShowMessage
などを利用することができます。 ShowMessage
で情報メッセージボックスを表示ShowWarningMessage
で警告メッセージボックスを表示ShowErrorMessage
で警告メッセージボックスを表示ShowOKCancelDialog
で「OK」/「キャンセル」のボタンを持つダイアログを表示ShowYesNoDialog
で「はい」/「いいえ」ボタンを持つダイアログ表示ShowYesNoCancelDialog
で「はい」/「いいえ」/「キャンセル」ボタンを持つダイアログを表示
- 単純なメッセージボックスは
- ファイルを開く、ファイル保存、フォルダ指定、といった代表的なシステムダイアログの表示
ShowOpenFileDialog
でファイルを開くダイアログを表示ShowSaveFileDialog
でファイルを保存ダイアログを表示SelectFolderMessage
をSendすることでフォルダの参照ダイアログを表示
また、簡便にカスタムダイアログを表示できるようにした機構も作られています。
ShowModalDialog
はカスタムダイアログをモーダルダイアログとして表示するために、 ShowDialog
や ShowModelessDialog
はモーダレスダイアログとして表示するために使用します。
カスタムダイアログの具体的な実装方法は後述するサンプルプログラムの解説にて説明します。
MVVM FrameworkとしてのMoNo.RAIL¶
MVVMとはモデル層とビュー層の間にビューモデル層を挟んだ、プレゼンテーションとドメインを分離するためのアーキテクチャパターンの一種です。 特にMoNo.RAILでは以下の方針で実装することを想定したフレームワーク設計になっています。
- ダイアログもViewとViewModelに分けて設計する。
- F#側にダイアログのViewModelを定義し、C#側にその外観(XAML)を定義する。
- MessengerにダイアログのViewModelを送ると、メッセージハンドラ側でビューと結び付けられてダイアログが表示される。
- ダイアログが閉じると、閉じた時点でのビューモデルの状態がそのままコマンド側に返ってくる。
上記に挙げた内容を踏まえてサンプルプログラムを確認してみましょう。
サンプルプロジェクトについて¶
ではMessengerSampleプロジェクトの内容を見ていきます。 まずはMessengerSample.ViewのMainWindow.xamlを見てみましょう。
...
<Window.DataContext>
<vm:ViewModel/>
</Window.DataContext>
<m:Messaging.Receiver>
<m:MessageReceiver Messenger="{Binding Messenger}"/>
</m:Messaging.Receiver>
<StackPanel>
<Button Command="{Binding Path=SampleMessageCommand}">Sample</Button>
<Button Command="{Binding Path=MessageBoxCommand}">MessageBox</Button>
<Button Command="{Binding Path=ModalDialogCommand}">ModalDialog</Button>
<Button Command="{Binding Path=ModelessDialogCommand}">ModelessDialog</Button>
</StackPanel>
...
Windowの DataContext
にViewModelが設定されており、4つのボタンにViewModelの各コマンドがバインドされているのがわかります。
また、 MessageReceiver
の Messenger
プロパティにViewModelの Messenger
がバインドされレシーバーに設定されています。
これによりViewModelからメッセージが送れるようになります。
ではViewModelの各コマンドを見てみます。
なお、このサンプルでは各コマンドは全て Wpf.Cont.toWpfCommand
関数に継続モナドを渡して作成しています。
ViewModelは MessengerContext
を継承しており、MessengerContext
は ContRunner
を継承しているため Wpf.Cont.toWpfCommand
の第1引数に自身を渡しています。
MessageBoxCommand¶
member val MessageBoxCommand =
cont {
do! this.ShowMessage "test"
}
|> Wpf.Cont.toWpfCommand this None
このコマンドはシンプルなメッセージボックスを表示します。
ShowMessage
はMoNo.RAILに組み込まれている、メッセージボックスを表示するためのメソッドです。
このメソッドは引数に渡された文字列をMoNo.RAILで定義されているメッセージに格納して送信しており、そのメッセージを処理するハンドラがメッセージボックスを起動します。
具体的には Wpf.InformationMessage
というメッセージを送信しています。
つまりこのコマンドは次のようにも書けます。
member val MessageBoxCommand =
cont {
do! this.Messenger.Send( Wpf.InformationMessage "test" )
}
|> Wpf.Cont.toWpfCommand this None
ShowMessage
だけでなく 前述の「組み込み済みのメッセージについて」で挙げた ShowWarningMessage
なども同様に組み込みメッセージを送っています。
これらのメソッドは「ダイアログを表示するメソッド」ではなく、「ダイアログを出してもらうようメッセージを送るメソッド」であることに注意して下さい。
ダイアログを表示するのはあくまでメッセージを処理するハンドラであり、そのハンドラはMoNo.RAILに組み込まれているだけです。
SampleMessageCommand¶
member val SampleMessageCommand =
cont {
let! result = this.Messenger.Send (SampleMessage 1234)
do! this.ShowMessage (sprintf "result = %d" result)
}
|> Wpf.Cont.toWpfCommand this None
このコマンドではメッセージを2回送っています。
まず Send
メソッドで SampleMessage
というメッセージを送っています。
次にそのハンドラでの処理結果を受けて ShowMessage
で結果を表示してもらうよう暗黙的にメッセージを送っているわけです。
SampleMessage
はViewModel.fsで定義しているカスタムメッセージです。
type SampleMessage (arg : int) =
member __.Arg = arg
interface Wpf.IMessage<int>
実質的にint型プロパティ Arg
を持つだけのメッセージです。
メッセージとして送れるよう Wpf.IMessage<int>
インターフェースを実装しています。
ジェネリック引数として指定している int
は、このメッセージの処理結果として int
の継続モナド Cont<int>
を受け取るという表明です。
ではこのメッセージはどのように処理されているのか、ハンドラを見てみましょう。 MessengerSample.Viewプロジェクト内のSampleMessageHandler.csを見て下さい。
namespace MessengerSample.View
{
[System.ComponentModel.Composition.Export(typeof(MoNo.Wpf.IMessageHandler))]
class SampleMessageHandler : MoNo.Wpf.MessageHandler<SampleMessage, int>
{
public override Cont<ISession, int> Handle( SampleMessage message )
{
return Cont.ofValue(2 * message.Arg);
}
}
}
Export(typeof(MoNo.Wpf.IMessageHandler))
属性がついているこのクラスをMoNo.RAILはハンドラクラスとしてプラグインします。
プラグインされたこのクラスは MoNo.Wpf.MessageHandler<SampleMessage, int>
を継承することで SampleMessage
を処理し int
の継続モナドを返すハンドラとなります。
そして Handle
メソッドでメッセージを処理します。
ここでは SampleMessage
の Arg
プロパティを2倍し、継続モナドとして返しています。
結局このコマンドは整数値をメッセージに包んで送り、ハンドラ側で2倍した結果を受けとってメッセージボックスに表示するコマンドであることがわかります。
ModalDialogCommandとModelessDialogCommand¶
この2つのコマンドはカスタムダイアログをモーダルダイアログまたはモードレスダイアログとして表示するコマンドです。
メッセージ送信に使うメソッドが ShowModalDialog
ShowModelessDialog
であることだけが異なります。
そのため以下ではModalDialogCommandを対象に説明します。
member val ModalDialogCommand =
cont {
let! vm = this.ShowModalDialog (VmSampleDialog())
printfn "Text = %s" vm.Text.Val
}
|> Wpf.Cont.toWpfCommand this None
ShowModalDialog
に VmSampleDialog
型のViewModelを渡してダイアログを表示し、ダイアログを閉じた後のViewModelを取得してその値を標準出力するというコマンドです。
まず ShowModalDialog
に渡している引数から見てみましょう。
ViewModel.fsの VmSampleDialog
という型を渡しています。
type VmSampleDialog() =
member val Text = Reactive "sample text"
interface Wpf.IDialogModel with
member __.DialogResult = true
文字列のReactive型の Text
プロパティを持ち、 Wpf.IDialogModel
を実装しています。
Wpf.IDialogModel
はダイアログのViewModelであることを示すインターフェースで、bool値 DialogResult
だけを持ちます。
[<Interface>]
type IDialogModel =
abstract member DialogResult : bool
次に ShowModalDialog
のシグネチャを確認してみましょう。
member Wpf.MessengerContext.ShowModalDialog : viewModel:`a -> Cont<'a>
これらのメソッドは「任意の型」を表示したいダイアログのViewModelとして渡すことができます。 Wpf.IDialogModel
を実装していなくても構いません。
シグネチャを見るとわかるように、これらのメソッドは引数として渡した型を継続モナドで包んだ型を返します。
引数に渡した型が Wpf.IDialogModel
を実装している場合は、ダイアログをどのように閉じたかが DialogResult
プロパティに設定されて返されるのです。
従って、ダイアログをどのように閉じたかが重要なケースでは Wpf.IDialogModel
を実装すべきでしょう。
ShowMessage
メソッドが引数に渡された文字列をメッセージに格納して送信していたように、 ShowModalDialog
は引数に渡されたオブジェクトとモーダルとして表示するかどうかの bool
値をメッセージに格納して送ります。
Wpf.DialogMessage<'a>
という型のメッセージです。
従って上記の ShowModalDialog
の箇所は以下のようにも書けます。
...
let! vm = this.Messenger.Send(Wpf.DialogMessage(VmSampleDialog(), true))
...
さて、最後にこのメッセージを処理するハンドラを見てみます。 MessengerSample.ViweプロジェクトのSampleDialog.xaml.csを見て下さい。
[System.ComponentModel.Composition.Export(typeof(MoNo.Wpf.IMessageHandler))]
class Handler : MoNo.Wpf.DialogMessageHandler<VmSampleDialog>
{
public Handler() : base(vm => new SampleDialog { DataContext = vm }) { }
}
MoNo.Wpf.DialogMessageHandler<VmSampleDialog>
という型を継承していることがわかります。
ShowModalDialog
に ViewModelである a
型を渡した場合のハンドラは Wpf.DialogMessageHandler<a>
を継承して作成します。
実際 Wpf.DialogMessageHandler<a>
は次のように定義されています。
public class DialogMessageHandler<a> : MessageHandler<DialogMessage<a>, a>
{
public DialogMessageHandler(Func<a, Window> createDialog);
public override Cont<ISession, a> Handle(DialogMessage<a> message);
}
これは Messagehandler<DialogMessage<a>, a>
を継承しているので、SampleMessageCommandで解説した通り「 DialogMessage<a>
型のメッセージを処理して a
の継続モナドを返すハンドラ」になっています。
ダイアログの表示など実際の処理の殆どはこのクラスに実装されていますので、このクラスを継承したクラスでは
Export(typeof(MoNo.Wpf.IMessageHandler))
属性を付けプラグインの対象にする- 「ViewModelを引数に取り、表示すべき
Window
を返す関数」を基底のコンストラクタに渡す
これだけでダイアログを表示するハンドラができ上がります。
(もちろん Handler
メソッドをオーバーライドして好きな処理を記述することもできますが DialogMessageHandler<a>
を継承する意味は殆どないでしょう。)
サンプルプログラムでは、
public Handler() : base(vm => new SampleDialog { DataContext = vm }) { }
と、「DataContextを初期化したSampleDialogを作成して返す関数」を基底クラスのコンストラクタに渡しています。 これにより渡したビューモデルを格納したSampleDialogを表示するハンドラを実現しています。