その代表的なものがウィンドウ遷移です。ViewModelが「ダイアログボックスを表示しなさい」という指示を出し、それを受け取ったViewがウィンドウを表示するという動作は、状態では表現できません。
ウィンドウも「表示状態」「非表示状態」の2種類の状態がある、と考えれば確かに状態で表現できそうな気もしますが、それはあまり自然な発想ではないですよね。
そのため、解決策として多くのMVVMライブラリにはそのような指示をViewModelからViewへ伝達する機能を備えています。
LivetではMessengerというものがそれに該当します。ViewModel側でInteractionMessageに伝えたい情報を載せてMessenger.Raiseすると、View側のInteractionMessageTriggerが発動し、その受け取ったメッセージをもとに様々な動作を行うことができます。
例えばメッセージボックスを表示する例です。
<Window x:Class="MessengerTest.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/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:l="http://schemas.livet-mvvm.net/2011/wpf" xmlns:v="clr-namespace:MessengerTest.Views" xmlns:vm="clr-namespace:MessengerTest.ViewModels" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> <i:Interaction.Triggers> <i:EventTrigger EventName="ContentRendered"> <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/> </i:EventTrigger> <i:EventTrigger EventName="Closed"> <l:DataContextDisposeAction/> </i:EventTrigger> <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Info"> <l:InformationDialogInteractionMessageAction /> </l:InteractionMessageTrigger> </i:Interaction.Triggers> <Grid> </Grid> </Window>
肝心のメッセージはこのように送ります。
public class MainWindowViewModel : ViewModel { public void Initialize() { Messenger.Raise(new InformationMessage("InformationMessage raised.", "Information", MessageBoxImage.Information, "Info")); } }
ポイントは、InteractionMessageとInteractionMessageActionは対になっていて、パラメーターの箱(Message)とそれを受けた動作(Action)をうまく定義することで、ViewModelからViewにいろいろな動作を働きかけることができるということです。
LivetでどのようなInteractionMessageやInteractionMessageActionが用意されているかというのは、以前書いたこちらの記事を参考にしてください。
InteractionMessageActionの自作
さて、このようなInteractionMessageActionを自作するにはどうしたら良いのでしょう。ScrollViewerを任意の場所にスクロールするInteractionMessageActionを例に取りながら説明していきます。
ScrollViewerにはスクロール位置を示すVerticalOffsetプロパティやHorizontalOffsetプロパティはあるのですが、これらは読み取り専用でスクロール位置を変更するのには使えません。スクロール位置を変更するにはScrollToVerticalOffsetメソッドやScrollToHorizontalOffsetメソッドを呼び出す必要があります。
メソッドの呼び出しはデータバインディングで行うことはできませんので、InteractionMessageActionに頼る必要があります。
まずはInteractionMessageを自作します。
public class ScrollInteractionMessage : InteractionMessage { public ScrollInteractionMessage() : base() { Command = ScrollCommand.None; Offset = 0; } public ScrollInteractionMessage(string messageKey) : base(messageKey) { Command = ScrollCommand.None; Offset = 0; } public ScrollInteractionMessage(ScrollCommand command) : base() { Command = command; Offset = 0; } public ScrollInteractionMessage(ScrollCommand command, string messageKey) : base(messageKey) { Command = command; Offset = 0; } public ScrollInteractionMessage(ScrollCommand command, double offset) : base() { Command = command; Offset = offset; } public ScrollInteractionMessage(ScrollCommand command, double offset, string messageKey) : base(messageKey) { Command = command; Offset = offset; } public ScrollCommand Command { get; set; } public double Offset { get; set; } protected override Freezable CreateInstanceCore() { return new ScrollInteractionMessage(Command, Offset, MessageKey); } }
ここで使っているScrollCommandはこのような列挙型です。
public enum ScrollCommand { None, Home, End, Top, Bottom, LeftEnd, RightEnd, VerticalOffset, HorizontalOffset, }
さて、このInteractionMessageの実装で重要なのが、CreateInstanceCoreのオーバーライドです。これをやらずにドツボにはまったことが私はありました。
InteractionMessageはFreezableを継承しており、Messenger.Raise中でFreezeされます。そしてその後にコピーされてInteractionMessageActionに渡されるのですが、その際、CreateInstanceCoreを呼び出してコピーされます。
これをオーバーライドしておかないと、InteractionMessage.CreateInstanceCoreでコピー作業が行われます。それはすなわち、InteractionMessageActionに渡されるのがInteractionMessage型のインスタンスとなってしまい追加のパラメーターがすべて消えることを意味しています。
ハマりポイントですので、ぜひ回避するようにしましょう。
また、当然ですが、CreateInstanceCoreメソッドでは同じパラメーターのインスタンスを生成するためのものですので、継承時に追加したプロパティはしっかりコピーするようにしてください。今回ならばCommandとOffsetですね。
これを受けるScrollInteractionMessageActionはこんな感じに実装しました。
public class ScrollInteractionMessageAction : InteractionMessageAction<FrameworkElement> { protected override void InvokeAction(InteractionMessage message) { if(AssociatedObject is ScrollViewer ctrl && message is ScrollInteractionMessage msg) { switch(msg.Command) { case ScrollCommand.Home: ctrl.ScrollToHome(); break; case ScrollCommand.End: ctrl.ScrollToEnd(); break; case ScrollCommand.Top: ctrl.ScrollToTop(); break; case ScrollCommand.Bottom: ctrl.ScrollToBottom(); break; case ScrollCommand.LeftEnd: ctrl.ScrollToLeftEnd(); break; case ScrollCommand.RightEnd: ctrl.ScrollToRightEnd(); break; case ScrollCommand.VerticalOffset: ctrl.ScrollToVerticalOffset(msg.Offset); break; case ScrollCommand.HorizontalOffset: ctrl.ScrollToHorizontalOffset(msg.Offset); break; } } } }
AssociatedObjectはこのActionがぶら下げられたコントロール、messageはMessenger.Raiseに渡されたメッセージです。受け取ったmessegeのCommandをもとに、呼び出すべきスクロールメソッドに制御を振り分けています。
さて、XAMLはこのようになります。
<ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <Image Source="{Binding FileName}" /> <i:Interaction.Triggers> <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll"> <a:ScrollInteractionMessageAction /> </l:InteractionMessageTrigger> </i:Interaction.Triggers> </ScrollViewer>
ViewModelからはこのようにMessenger.Raiseを呼び出すだけです。
ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll"))); ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll"))); ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll"))); ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll")));
ボタンを押すだけで四隅へ移動できるソフトが出来上がりました。
参考までにViewとViewModelの全容を載せておきます。
<Window x:Class="MessengerTest.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/expression/2010/interactivity" xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" xmlns:l="http://schemas.livet-mvvm.net/2011/wpf" xmlns:v="clr-namespace:MessengerTest.Views" xmlns:a="clr-namespace:MessengerTest.Views.Actions" xmlns:vm="clr-namespace:MessengerTest.ViewModels" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <vm:MainWindowViewModel/> </Window.DataContext> <i:Interaction.Triggers> <i:EventTrigger EventName="ContentRendered"> <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/> </i:EventTrigger> <i:EventTrigger EventName="Closed"> <l:DataContextDisposeAction/> </i:EventTrigger> </i:Interaction.Triggers> <Grid> <DockPanel> <Grid DockPanel.Dock="Top"> <Grid.ColumnDefinitions> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="1*" /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Content="Left" Command="{Binding ScrollToLeft}" Margin="2" /> <Button Grid.Column="1" Content="Top" Command="{Binding ScrollToTop}" Margin="2" /> <Button Grid.Column="2" Content="Bottom" Command="{Binding ScrollToBottom}" Margin="2" /> <Button Grid.Column="3" Content="Right" Command="{Binding ScrollToRight}" Margin="2" /> </Grid> <Grid DockPanel.Dock="Bottom"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="1*" /> <ColumnDefinition Width="100" /> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Path: " VerticalAlignment="Center" Margin="2" /> <TextBox Grid.Column="1" Text="{Binding FileName}" VerticalAlignment="Center" Margin="2" /> <Button Grid.Column="2" Content="Browse..." VerticalAlignment="Center" Margin="2"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <l:OpenFileDialogInteractionMessageAction> <l:DirectInteractionMessage CallbackMethodTarget="{Binding}" CallbackMethodName="FileSelected"> <l:OpeningFileSelectionMessage Filter="Image file (*.jpg;*.png;*.gif;*.bmp)|*.jpg;*.png;*.gif;*.bmp" MultiSelect="False" /> </l:DirectInteractionMessage> </l:OpenFileDialogInteractionMessageAction> </i:EventTrigger> </i:Interaction.Triggers> </Button> </Grid> <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible"> <Image Source="{Binding FileName}" /> <i:Interaction.Triggers> <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Scroll"> <a:ScrollInteractionMessageAction /> </l:InteractionMessageTrigger> </i:Interaction.Triggers> </ScrollViewer> </DockPanel> </Grid> </Window>
public class MainWindowViewModel : ViewModel { public MainWindowViewModel() { ScrollToLeft = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.LeftEnd, "Scroll"))); ScrollToRight = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.RightEnd, "Scroll"))); ScrollToTop = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Top, "Scroll"))); ScrollToBottom = new ViewModelCommand(() => Messenger.Raise(new ScrollInteractionMessage(ScrollCommand.Bottom, "Scroll"))); } public void Initialize() { } #region FileName変更通知プロパティ private string _FileName; public string FileName { get { return _FileName; } set { if(_FileName == value) return; _FileName = value; RaisePropertyChanged(nameof(FileName)); } } #endregion public ViewModelCommand ScrollToLeft { get; } public ViewModelCommand ScrollToRight { get; } public ViewModelCommand ScrollToTop { get; } public ViewModelCommand ScrollToBottom { get; } public void FileSelected(OpeningFileSelectionMessage msg) { if(msg.Response != null && msg.Response.Length == 1) { FileName = msg.Response.Single(); } } }
おまけ
Livetを使っていると気づくかもしれませんが、WindowActionMessageをMessenger.Raiseしたときにうまく動きません。DirectInteractionMessage経由(XAMLで全部記述)ならばうまくいくのですが。ですので、ViewModel側からウィンドウ操作をしたい場合は、WindowActionごとにInteractionMessageTriggerを用意して、キーで呼び分ける必要がでてきます。
なぜこんなことになっているのかというと、ただのバグです。
WindowActionMessageのCreateInstanceCoreのオーバーライドにてWindowActionのコピーを忘れています。ですので、Messenger.RaiseでどんなWindowActionを設定してもここの時点で闇に葬られるわけですね。
DirectInteractionMessageだとCreateInstanceCoreは呼ばれない(元のインスタンスのまま渡る)ので、設定した値がそのまま生かされます。
ま、もうLivetは事実上開発が止まっていますので、自分で修正してビルドして使うなりなんなりしてやるしかないですね。はい。
0 件のコメント:
コメントを投稿