.. :tocdepth:2
============================
Messenger
============================
Messenger パターン
============================
一般に WPF/MVVM フレームワークには Messenger と呼ばれる機能が必要となります。
MVVMパターンではView層がViewModel層を参照しますが、逆向きの依存関係はありません。
つまりViewModel層からView層を見ることは出来ません::
View -----> ViewModel
では、View に定義したダイアログを ViewModel から起動したい場合はどうすれば良いのでしょうか。
依存関係がありませんから、ダイアログを ViewModel から直接起動することは出来ません。
この問題を解決するのが Messenger パターンです。
ダイアログを直接起動するのではなく、Messenger を経由して間接的にダイアログを起動するのです。
1. ViewModel は Messenger に「Xxxという名前のダイアログを起動して下さい」というメッセージを送ります。
2. Messenger はそのメッセージをView層に送ります。
3. Viewに設定されたハンドラーがそのメッセージを解釈し、ダイアログを起動します。
MoNo.RAIL の Messenger 機能
============================
MoNo.RAIL が備える Messenger 機構は次のような構成になっています。
.. image:: messenger.png
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を見てみましょう。
.. code-block:: xml
...
...
Windowの ``DataContext`` にViewModelが設定されており、4つのボタンにViewModelの各コマンドがバインドされているのがわかります。
また、 ``MessageReceiver`` の ``Messenger`` プロパティにViewModelの ``Messenger`` がバインドされレシーバーに設定されています。
これによりViewModelからメッセージが送れるようになります。
ではViewModelの各コマンドを見てみます。
なお、このサンプルでは各コマンドは全て ``Wpf.Cont.toWpfCommand`` 関数に継続モナドを渡して作成しています。
ViewModelは ``MessengerContext`` を継承しており、``MessengerContext`` は ``ContRunner`` を継承しているため ``Wpf.Cont.toWpfCommand`` の第1引数に自身を渡しています。
MessageBoxCommand
-----------------------------
.. code-block:: fsharp
member val MessageBoxCommand =
cont {
do! this.ShowMessage "test"
}
|> Wpf.Cont.toWpfCommand this None
このコマンドはシンプルなメッセージボックスを表示します。
``ShowMessage`` はMoNo.RAILに組み込まれている、メッセージボックスを表示するためのメソッドです。
このメソッドは引数に渡された文字列をMoNo.RAILで定義されているメッセージに格納して送信しており、そのメッセージを処理するハンドラがメッセージボックスを起動します。
具体的には ``Wpf.InformationMessage`` というメッセージを送信しています。
つまりこのコマンドは次のようにも書けます。
.. code-block:: fsharp
member val MessageBoxCommand =
cont {
do! this.Messenger.Send( Wpf.InformationMessage "test" )
}
|> Wpf.Cont.toWpfCommand this None
``ShowMessage`` だけでなく 前述の「組み込み済みのメッセージについて」で挙げた ``ShowWarningMessage`` なども同様に組み込みメッセージを送っています。
これらのメソッドは「ダイアログを表示するメソッド」ではなく、「ダイアログを出してもらうようメッセージを送るメソッド」であることに注意して下さい。
ダイアログを表示するのはあくまでメッセージを処理するハンドラであり、そのハンドラはMoNo.RAILに組み込まれているだけです。
SampleMessageCommand
-------------------------------
.. code-block:: fsharp
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で定義しているカスタムメッセージです。
.. code-block:: fsharp
type SampleMessage (arg : int) =
member __.Arg = arg
interface Wpf.IMessage
実質的にint型プロパティ ``Arg`` を持つだけのメッセージです。
メッセージとして送れるよう ``Wpf.IMessage`` インターフェースを実装しています。
ジェネリック引数として指定している ``int`` は、このメッセージの処理結果として ``int`` の継続モナド ``Cont`` を受け取るという表明です。
ではこのメッセージはどのように処理されているのか、ハンドラを見てみましょう。
MessengerSample.Viewプロジェクト内のSampleMessageHandler.csを見て下さい。
.. code-block:: csharp
namespace MessengerSample.View
{
[System.ComponentModel.Composition.Export(typeof(MoNo.Wpf.IMessageHandler))]
class SampleMessageHandler : MoNo.Wpf.MessageHandler
{
public override Cont Handle( SampleMessage message )
{
return Cont.ofValue(2 * message.Arg);
}
}
}
``Export(typeof(MoNo.Wpf.IMessageHandler))`` 属性がついているこのクラスをMoNo.RAILはハンドラクラスとしてプラグインします。
プラグインされたこのクラスは ``MoNo.Wpf.MessageHandler`` を継承することで ``SampleMessage`` を処理し ``int`` の継続モナドを返すハンドラとなります。
そして ``Handle`` メソッドでメッセージを処理します。
ここでは ``SampleMessage`` の ``Arg`` プロパティを2倍し、継続モナドとして返しています。
結局このコマンドは整数値をメッセージに包んで送り、ハンドラ側で2倍した結果を受けとってメッセージボックスに表示するコマンドであることがわかります。
ModalDialogCommandとModelessDialogCommand
-------------------------------------------
この2つのコマンドはカスタムダイアログをモーダルダイアログまたはモードレスダイアログとして表示するコマンドです。
メッセージ送信に使うメソッドが ``ShowModalDialog`` ``ShowModelessDialog`` であることだけが異なります。
そのため以下ではModalDialogCommandを対象に説明します。
.. code-block:: fsharp
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`` という型を渡しています。
.. code-block:: fsharp
type VmSampleDialog() =
member val Text = Reactive "sample text"
interface Wpf.IDialogModel with
member __.DialogResult = true
文字列のReactive型の ``Text`` プロパティを持ち、 ``Wpf.IDialogModel`` を実装しています。
``Wpf.IDialogModel`` はダイアログのViewModelであることを示すインターフェースで、bool値 ``DialogResult`` だけを持ちます。
.. code-block:: fsharp
[]
type IDialogModel =
abstract member DialogResult : bool
次に ``ShowModalDialog`` のシグネチャを確認してみましょう。
.. code-block:: fsharp
member Wpf.MessengerContext.ShowModalDialog : viewModel:`a -> Cont<'a>
これらのメソッドは「任意の型」を表示したいダイアログのViewModelとして渡すことができます。 ``Wpf.IDialogModel`` を実装していなくても構いません。
シグネチャを見るとわかるように、これらのメソッドは引数として渡した型を継続モナドで包んだ型を返します。
引数に渡した型が ``Wpf.IDialogModel`` を実装している場合は、ダイアログをどのように閉じたかが ``DialogResult`` プロパティに設定されて返されるのです。
従って、ダイアログをどのように閉じたかが重要なケースでは ``Wpf.IDialogModel`` を実装すべきでしょう。
``ShowMessage`` メソッドが引数に渡された文字列をメッセージに格納して送信していたように、 ``ShowModalDialog`` は引数に渡されたオブジェクトとモーダルとして表示するかどうかの ``bool`` 値をメッセージに格納して送ります。
``Wpf.DialogMessage<'a>`` という型のメッセージです。
従って上記の ``ShowModalDialog`` の箇所は以下のようにも書けます。
.. code-block:: fsharp
...
let! vm = this.Messenger.Send(Wpf.DialogMessage(VmSampleDialog(), true))
...
さて、最後にこのメッセージを処理するハンドラを見てみます。
MessengerSample.ViweプロジェクトのSampleDialog.xaml.csを見て下さい。
.. code-block:: csharp
[System.ComponentModel.Composition.Export(typeof(MoNo.Wpf.IMessageHandler))]
class Handler : MoNo.Wpf.DialogMessageHandler
{
public Handler() : base(vm => new SampleDialog { DataContext = vm }) { }
}
``MoNo.Wpf.DialogMessageHandler`` という型を継承していることがわかります。
``ShowModalDialog`` に ViewModelである ``a`` 型を渡した場合のハンドラは ``Wpf.DialogMessageHandler`` を継承して作成します。
実際 ``Wpf.DialogMessageHandler`` は次のように定義されています。
.. code-block:: csharp
public class DialogMessageHandler : MessageHandler, a>
{
public DialogMessageHandler(Func createDialog);
public override Cont Handle(DialogMessage message);
}
これは ``Messagehandler, a>`` を継承しているので、SampleMessageCommandで解説した通り「 ``DialogMessage`` 型のメッセージを処理して ``a`` の継続モナドを返すハンドラ」になっています。
ダイアログの表示など実際の処理の殆どはこのクラスに実装されていますので、このクラスを継承したクラスでは
* ``Export(typeof(MoNo.Wpf.IMessageHandler))`` 属性を付けプラグインの対象にする
* 「ViewModelを引数に取り、表示すべき ``Window`` を返す関数」を基底のコンストラクタに渡す
これだけでダイアログを表示するハンドラができ上がります。
(もちろん ``Handler`` メソッドをオーバーライドして好きな処理を記述することもできますが ``DialogMessageHandler`` を継承する意味は殆どないでしょう。)
サンプルプログラムでは、
.. code-block:: fsharp
public Handler() : base(vm => new SampleDialog { DataContext = vm }) { }
と、「DataContextを初期化したSampleDialogを作成して返す関数」を基底クラスのコンストラクタに渡しています。
これにより渡したビューモデルを格納したSampleDialogを表示するハンドラを実現しています。