ViewModelから画面遷移する[Xamarin.Forms]
様の投稿を見て思いつきました。ネタというか個人的に好きな方法で実用性はわかりません。
MVVMでViewModelからViewへの操作は難しいです。理由は上記投稿を見ていただければ大丈夫なので説明はしません。
ただ、個人的にViewへのコードビハインドは何も書きたくない(願望)ですし、なんとなくViewModelからViewを生成したくない、画面遷移だけでライブラリをいれたくないという超わがままな理由から第5の方法を紹介します。
それは
Messengerを自作する
です。
いやいやMessagingCenterがあるじゃん!!思うかもしれませんがMessagingCenterはstaticであり思わぬところで誤爆する可能性があ・・・りますかね? あとどうせなら自分で作ってカスタマイズしたいですよねそうですよね!!
では実際に作っていきましょう。
Messenger
今回は簡単なMessengerを作ります。
作り方は
を参考にしています。というかほとんど同じです。この記事はWPFの解説記事ですし書かれた時期はXamarin社ができたのとほぼ同じですがC#の基本的な機能を使っているのでView部分以外はそのまま使えます。素晴らしい!!
Message.cs
MessengerのMessage部分です。ここにViewへのメッセージを詰め込みます。今回はResponseを受け取る予定はないのでデフォルト引数でnullを指定しています。
using System; namespace MessengerSample { public class Message { //メッセージ本体 public object Body { get;} //レスポンス(今回は未使用) public object Response { get; } public Message(object body, object response=null) { this.Body=body; Response = response; } } }
MessageEventArgs.cs
Messengerのイベントのイベント引数です。今回は特にコールバックを以下略
using System; namespace MessengerSample { public class MessageEventArgs:EventArgs { public Message Message { get; } public Action<Message> Callback { get;} public MessageEventArgs(Message message, Action<Message> callback=null) { this.Message = message; this.Callback = callback; } } }
Messenger.cs
Messageを発行するクラスです。今回は特にコールバックを(ry
using System; namespace MessengerSample { public class Messenger { //Messageが送信されたことを通知するイベント public event EventHandler<MessageEventArgs> Raised; //Messageを送信する public void Raise(Message message, Action<Message> callback=null) { this.Raised?.Invoke(this,new MessageEventArgs(message,callback)); } } }
ViewModelBase.cs
これでMessengerの用意ができたのでこれを使っていくクラスを作っていきます。
using System.ComponentModel; using System.Runtime.CompilerServices; using MessengerSample.Annotations; namespace MessengerSample { public class ViewModelBase:INotifyPropertyChanged { public Messenger Messenger { get; }=new Messenger(); public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
このViewModelBaseを継承することでMessengerを使ってViewに通知をだすことができます。 あとなぜかINotifyPropertyChangedを継承していますが特に必要ありません。
MessageTriggerBehavior.cs
次にMessageを受信します。上でも述べたとおりViewのコードビハインドに書くのは避けたいのでBehaviorを使います。
using System; using Xamarin.Forms; namespace MessengerSample { public class MessageTriggerBehavior : Behavior<Page> { private Page _page; public static readonly BindableProperty MessengerProperty = BindableProperty.Create(nameof(Messenger), typeof (Messenger), typeof (MessageTriggerBehavior), default(Messenger)); //ViewModelのMessenger public Messenger Messenger { get { return (Messenger) GetValue(MessengerProperty); } set { SetValue(MessengerProperty, value); } } public static readonly BindableProperty MessageKeyProperty = BindableProperty.Create(nameof(MessageKey), typeof (string), typeof (MessageTriggerBehavior), default(string)); //このMessageKeyと一致したとき遷移させる public string MessageKey { get { return (string) GetValue(MessageKeyProperty); } set { SetValue(MessageKeyProperty, value); } } public static readonly BindableProperty PageTypeProperty = BindableProperty.Create(nameof(PageType), typeof (Type), typeof (MessageTriggerBehavior), default(Type)); //遷移先ページのタイプ public Type PageType { get { return (Type) GetValue(PageTypeProperty); } set { SetValue(PageTypeProperty, value); } } protected override void OnAttachedTo(Page bindable) { base.OnAttachedTo(bindable); _page = bindable; //これしないとBehaviorのプロパティに値がバインドされない this.BindingContext = bindable.BindingContext; //Messengerに登録 Messenger.Raised += Invoke; } protected override void OnDetachingFrom(Page bindable) { Messenger.Raised -= Invoke; this.BindingContext = null; base.OnDetachingFrom(bindable); } private void Invoke(object sender, MessageEventArgs args) { var message = args.Message.Body as string; //MessageKeyと送られてきたMessage.Bodyがあっていなければ何もしない if (message == null || MessageKey != message) return; //PageTypeからPageインスタンス作成 var page = Activator.CreateInstance(PageType) as Page; //遷移 _page.Navigation.PushAsync(page); } } }
MessengerにはViewModelBaseのMessengerをバインドし、MessengerKeyにはMessengerのイベントハンドラに登録したMessage.Bodyと同じ文字列を指定します。
MessageKeyと同じMessage.Bodyを受信するとPageTypeからインスタンスを生成し、Navigation.PushAsyncする単純な仕組みです。
これで準備が終わりました。長い
使ってみる
Content1.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:messengerSample="clr-namespace:MessengerSample;assembly=MessengerSample" x:Class="MessengerSample.Content1"> <ContentPage.BindingContext> <messengerSample:ContentViewModel /> </ContentPage.BindingContext> <ContentPage.Behaviors> <messengerSample:MessageTriggerBehavior Messenger="{Binding Messenger}" MessageKey="Show:Content2" PageType="{x:Type messengerSample:Content2}" /> </ContentPage.Behaviors> <StackLayout HorizontalOptions="Center" VerticalOptions="Center"> <Label Text="Content1" FontSize="Large" /> <Button Text="Navigate Content2" Command="{Binding NavigationContent2}" /> </StackLayout> </ContentPage>
Content2.xaml
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:messengerSample="clr-namespace:MessengerSample;assembly=MessengerSample" x:Class="MessengerSample.Content2"> <ContentPage.BindingContext> <messengerSample:ContentViewModel /> </ContentPage.BindingContext> <ContentPage.Behaviors> <messengerSample:MessageTriggerBehavior Messenger="{Binding Messenger}" MessageKey="Show:Content1" PageType="{x:Type messengerSample:Content1}" /> </ContentPage.Behaviors> <StackLayout HorizontalOptions="Center" VerticalOptions="Center"> <Label Text="Content2" FontSize="Large" TextColor="Green" /> <Button Text="Navigation Content1" Command="{Binding NavigationContent1}" /> </StackLayout> </ContentPage>
ContentViewModel.cs
using System; using Reactive.Bindings; namespace MessengerSample { public class ContentViewModel : ViewModelBase { //Content1に遷移するコマンド public ReactiveCommand NavigationContent1 { get; } = new ReactiveCommand(); //Content2に遷移するコマンド public ReactiveCommand NavigationContent2 { get; } = new ReactiveCommand(); public ContentViewModel() { NavigationContent1.Subscribe(_ => Messenger.Raise(new Message("Show:Content1"))); NavigationContent2.Subscribe(_ => Messenger.Raise(new Message("Show:Content2"))); } } }
Content1のボタンをクリックするとCommandが実行しViewに向けてメッセージが飛びます。それをBehaviorで受け止めてリフレクションでContent2のインスタンスを作成し遷移させます。
今回はわざわざICommandを継承したクラスを作成して〜をするのがめんどくさかったので ReactiveProperty でコマンドを生成しています。
これを実行すると・・・
Navigation Content2を押せば
無事遷移できました!! ちなみにサンプルがiOSだけなのはAndroidのシュミレータはなぜか起動しないし(実機持ってない)、UWPは’アプリは開始されませんでした’エラーでいつもどおり実行できなかったので実行できたのがiOSだけだからです。
画面遷移時のデータ引き渡しについて
画面遷移時のデータ引き渡しにはMessageを拡張し、そこに遷移先のViewModelのインスタンスを突っ込んでRaiseしMessageTriggerBehaviorでごにょごにょすれば大丈夫だと思います。
まとめ
ライブラリでいいんじゃね?
この方法でも実際に画面遷移ができました。この方法の利点は自分で好きな機能を継ぎ足せることにあると思います。
また余計な機能がないのでとてもシンプルです。他の利点は・・・特に思いつきません。。
参考
Xamarin.Forms ページ遷移時のデータ受け渡しについて - Qiita
Xamarin.Forms に CallMethodAction が無かったので Behavior で代用してみた - Qiita