2018年1月17日水曜日

LivetにおけるWPFでのダイアログ表示のいろは

いっつもLivetを使っているはずなのに、いろいろなクラスの名前をすっかり忘れてしまってその都度ググることになってしまっているので、備忘録的にまとめます。

基本

TriggerAction

画面遷移は基本的にTriggerActionを使います。その名の通り、何かしらの引き金によって発動するアクションです。
TriggerActionを発動させる方法はいくつかありますが、イベントで発動させる方法とViewModelから発動させる方法を覚えておけば困らないでしょう。前者はEventTrigger(System.Windows.Interactivityの機能)、後者はMessenger(Livetの機能)を使います。

EventTrigger

こちらは、例えばダイアログ表示ではなくても、Livetのメソッド直接バインディング機能などを使うときに多用することになるでしょう。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="ButtonPushed" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
例えばこんなやつです。ボタンを押すとEventTriggerでClickイベントを拾い、LivetCallMethodActionを発動させるコードです。基本的にレイアウトなどの動きが無いコードになりがちなマークアップ言語に一気に彩りが生まれる記法ですので是非押さえておいてください。

Messenger

LivetにはMessengerという機能があります。ViewModelはMessengerというインスタンスを持っており、MessengerでInteractionMessageクラスのメッセージを送信すると、それをViewで受け取れる(=TriggerActionとして動作する)というものです。
標準のWPFにはこのようにViewModelからViewに何かを働きかける機能がかなり少なく、Livet以外のMVVMライブラリでもたいてい似たような機能は実装されているようです。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MethodCaller">
        <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="MessageRaised" />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
これは上記のEventTriggerの例になぞらえて、代わりにMessengerでLivetCallMethodActionを発動させるプログラムです。EventTriggerがInteractionMessageTriggerに代わっただけですので難なく理解できるかと思います。
ちなみに、ViewModelではこのように呼び出します。
Messenger.Raise(new InteractionMessage("MethodCaller"));
InteractionMessageクラスでキーを指定します。これによって、単一のコントロール内に複数のInteractionMessageTriggerを持っていても使い分けることができます。

InteractionMessage

上記のMessengerの項でも少し出てきましたが、MessengerとはViewModelからViewに情報を受け渡すものです。上記の例ではRaiseしかしていないのでMessengerKeyしか渡しませんでしたが、例えばダイアログボックスならば表示テキストやキャプション、アイコンの種類などを伝える必要があるでしょう。そのような場合は、InteractionMessageを継承した派生クラスを作ってパラメーターを持たせるような仕様になっています。それに対応して、TriggerActionの派生クラスであるInteractionMessageActionを継承した派生クラスも作成し、そこで独自のInteractionMessageを受け取れば良いです。

Messengerの例

というわけで、まずはMessengerを使ってダイアログボックスを表示する例を示します。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:InformationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
Messenger.Raise(new InformationMessage("Message Text", "Caption", MessageBoxImage.Information, "MessageBox"));
ViewとViewModelはこんな感じになります。
InformataionDialogInteractionMessageActionは単一ボタン(=OKボタンのみ)のメッセージボックスを表示するTriggerActionです。
これに対応するInteractionMessageはInformationMessageで、これにメッセージ内容、キャプション内容、アイコン、メッセージキーを含めてMessengerに乗せることでダイアログボックスを表示させることができます。

EventTriggerの例

ところで、ボタンが押されたらそのままメッセージボックスを表示したくなることがあります。ですが、上記のMessengerの例を見ているとViewModelからViewにメッセージを送る必要があるので、ボタン(View)—(メソッド呼び出し)→Messenger発動(ViewModel)—(Messenger)→ダイアログ表示(View)といった具合にViewとViewModelを往復する必要が出てきてしまいます。これではあまりスマートではありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
InteractionMessageActionクラスにはDirectInteractionMessageプロパティが用意されており、ここに直接InteractionMessageを渡してあげればいい…と思いきや、ここに渡すのはDirectInteractionMessageクラスです。これについては細かくは後述します。
DirectInteractionMessageにはMessageプロパティがあり、こちらにInteractionMessageを渡せばいいです。
さらに、それぞれのクラスのDirectInteractionMessageプロパティ、MessageプロパティはContentPropertyに設定されているので、プロパティを明示しなくても設定できるのでネストが深くなるのも最小限に抑えることができます。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:InformationDialogInteractionMessageAction>
                <l:DirectInteractionMessage>
                    <l:InformationMessage Caption="Caption" Text="Message text" Image="Information" />
                </l:DirectInteractionMessage>
            </l:InformationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
こんな感じです。InformataionDialogInteractionMessageActionに直接InformationMessageを渡すことができています。これによって、View単体で(ViewModelを介在せずに)ダイアログを表示することができます。

ダイアログの戻り値

さて、上記は単一ボタンのダイアログを表示していたので戻り値は特に必要ありませんでした。ですが、例えば「はい」「いいえ」で聞くダイアログなどは戻り値を知る必要があります。

Messengerの例

実はこの戻り値はMessengerが持つようにできています。ですので、Messengerの例は特に上記の例とほぼ同じコードになります。
<i:Interaction.Triggers>
    <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="MessageBox">
        <l:ConfirmationDialogInteractionMessageAction />
    </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
var msg = new ConfirmationMessage("Is this a MessageBox?", "Caption", MessageBoxImage.Question, MessageBoxButton.YesNoCancel, "MessageBox");
Messenger.Raise(msg);
if(msg.Response == null) {
    // Cancel clicked
} if(msg.Response.Value) {
    // Yes clicked
} else {
    // No clicked
}
bool?型のResponseプロパティに結果が入るので、Messaenger.Raiseから制御が返ってきた後にそれを見れば問題ありません。

EventTriggerの例

さて、ここでまた問題が発生します。当然、ダイアログの戻り値はソフトウェアの制御側(=ViewModel)で受け取りたいですが、EventTriggerで直接TriggerActionを呼び出してしまった場合、ViewModelは介在しないのでそのMessageを受け取るところがありません。

ですがそこは流石Livet、ちゃんと方法が用意されています。
先ほど無駄に挟んでいたように見えたDirectInteractionMessageクラスですが、実は答えはここにあります。これがCallbackMethodTarget/CallbackMethodNameプロパティ及びCallbackCommandプロパティを持っていて、ここでViewModelのメソッドやコマンドをバインディングすることによってViewModelが戻り値を受け取ることができるようになります。
<Button Content="Push me" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="30" >
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click" >
            <l:ConfirmationDialogInteractionMessageAction>
                <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="MessageBoxClosed">
                    <l:ConfirmationMessage Caption="Is this a MessageBox?" Text="Message text" Image="Question" Button="YesNoCancel" />
                </l:DirectInteractionMessage>
            </l:ConfirmationDialogInteractionMessageAction>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>
public void MessageBoxClosed(ConfirmationMessage msg)
{
    if(msg.Response == null) {
        // Cancel clicked
    }
    if(msg.Response.Value) {
        // Yes clicked
    } else {
        // No clicked
    }
}
ViewModelではDirectInteractionMessageに渡していたInteractionMessageをパラメーターにしたメソッドを用意しておく必要があります。これで、ボタンからメッセージボックスを表示し、その結果をViewModelで受け取ることができます。

クラス名対応表

さて、Messaengerとかはもうわかっているからいいよという人はここだけ見てください。
用途 TriggerAction InteractionMessage 備考
メッセージボックス(単一ボタン) InformationDialogInteractionMessageAction InformationMessage
メッセージボックス(複数ボタン) ConfirmationDialogInteractionMessageAction ConfirmationMessage
ファイルを開くダイアログ OpenFileDialogInteractionMessageAction OpeningFileSelectionMessage
ファイルを保存ダイアログ SaveFileDialogInteractionMessageAction SavingFileSelectionMessage
フォルダ選択ダイアログ FolderBrowserDialogInteractionMessageAction FolderSelectionMessage Livet.Extensionsが必要
任意のウィンドウ TransitionInteractionMessageAction TransitionMessage
Livetが持っているダイアログ表示関係のTriggerAction/InteractionMessageはこれで全部のはずです。ほかにも、Windowを最大化/最小化/閉じるなどを実現するWindowInteractionMessageAction/WindowActionMessageなどもありますが、まあこれは自分で調べて使ってみてください。

最後の任意のウィンドウ表示はなかなかに曲者かと思います。特に、これで開くウィンドウに対応するViewModelをどうしたらいいのか、というところでとても悩むことになるかと思います。
まあその辺はまたの機会に。

0 件のコメント:

コメントを投稿