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 機構は次のような構成になっています。

../../_images/messenger.png

MoNo.RAIL の MVVM 機構は、ビュー側のコードビハインドを完全にゼロにするフルMVVMではありません。 上図の CustomMessageHandler の部分はビュー側に C# で記述することになります。

ViewModelクラスは MessengerContext の派生クラスとして定義します。 MessengerContextMessenger を保持しており、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 はカスタムダイアログをモーダルダイアログとして表示するために、 ShowDialogShowModelessDialog はモーダレスダイアログとして表示するために使用します。

カスタムダイアログの具体的な実装方法は後述するサンプルプログラムの解説にて説明します。

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の各コマンドがバインドされているのがわかります。

また、 MessageReceiverMessenger プロパティにViewModelの Messenger がバインドされレシーバーに設定されています。 これによりViewModelからメッセージが送れるようになります。

ではViewModelの各コマンドを見てみます。 なお、このサンプルでは各コマンドは全て Wpf.Cont.toWpfCommand 関数に継続モナドを渡して作成しています。 ViewModelは MessengerContext を継承しており、MessengerContextContRunner を継承しているため 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 メソッドでメッセージを処理します。

ここでは SampleMessageArg プロパティを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

ShowModalDialogVmSampleDialog 型の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を表示するハンドラを実現しています。