Xamarin逆引きTips
MvvmCrossでViewModelからViewにイベントを通知するには?(Messengerパターン)
MvvmCrossでのiOS/Androidアプリ開発において、ViewModelからViewにイベントを通知するMessengerパターンの実装方法を紹介する。
MVVMを使った開発では、ViewModelからViewに対する値の反映は主にバインディングを使用する。しかし、メッセージダイアログ(アラート)*1の表示や、プラットフォーム固有機能の呼び出しの際は、ViewModelからViewに対してイベントを通知したい場合がある。このようなイベントの通知には、Messengerという機構を使用できる。MvvmCrossでは、Messengerはプラグインとして提供されている。
- *1 用語として、iOSでは「アラート」が、Androidでは「ダイアログ」が用いられる。今回は以後、ダイアログで共通化する。
Messengerの仕組み
Messengerは、送信側となるPublisherから、受信側となるSubscriberにメッセージを送る、イベント通知機構のひとつで、Subscriberがメッセージの種類に対して受信先を登録しておくと(図1の1)、Publisherによりメッセージが送られたタイミングで(2)、そのメッセージを登録されている全てのSubscriberに送信する(3)。
まずSubscribeでメッセージを受信するメソッド/デリゲートを登録(1)。その後、ViewModelやServiceからMessageをPublishすると(2)、Subscribeで登録したメソッドが実行される(3)。同じ種類のメッセージに対して複数のメソッドを受信登録でき、複数の受信登録がある種類のメッセージをPublishすると全ての登録先にメッセージが配信される
MvvmCrossのMessengerプラグインでは、PublisherやSubscriberはIoCコンテナーから取得したIMvxMessenger
インターフェースのオブジェクトを通じてデリゲートを登録し、メッセージを送る。送るメッセージはクラスで表し、その中に値を持たせることもできる。
このようなイベント通知機構自体は各プラットフォーム固有でも存在しており、iOSの場合はNSNotificationCenter、Androidの場合はLocalBroadcastManagerが使用できる。特にNSNotificationCenterは、アプリの状態変更(起動・停止)や画面回転など、フレームワークやシステムからの情報の送信にも用いられている。
Messengerプラグインを使用したメッセージダイアログの表示
ここでは、Messengerプラグインを使ってメッセージダイアログを表示する方法を解説する。
プロジェクトの準備
「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。次に「Tips:MvvmCrossでWebBrowserプラグインを使用するには?」でWebBrowserプラグインを追加する際と同様の手順で、NuGetから「MvvmCross - Messenger Plugin (MvvmCross.HotTuna.Plugin.Messenger)」を各プロジェクトに追加する。
メッセージの定義
次に、PublisherとSubscriber間でやりとりするメッセージを表すクラスを定義する。MessengerSample.Coreプロジェクトを右クリックし、(それにより表示されるコンテキストメニューの)[追加]の中から[新しいフォルダ]を選択してフォルダーを作成する。そのフォルダーにはMessagesと名前を指定する。次にMessagesフォルダーを右クリックして[追加]の中から[新しいファイル]を選択する。開いた[新しいファイル]ダイアログで[空のクラス]を選択し、ShowAlertMessageと名前を付け、ファイルを作成する。
そのShowAlertMessage.csファイルの内容を以下のように編集する。
using System;
using Cirrious.MvvmCross.Plugins.Messenger;
namespace MessengerSample.Core.Messages
{
public class ShowAlertMessage : MvxMessage // 1
{
public string AlertMessage // 2
{
get;
private set;
}
public ShowAlertMessage(object sender, string alertMessage) : base(sender) // 3
{
this.AlertMessage = alertMessage;
}
}
}
|
メッセージはMvxMessage
クラスから派生する必要がある(1)。メッセージクラスには、このメッセージで渡したい情報を、プロパティとして持たせておくこともできる。今回は表示するテキストをAlertMessage
プロパティとして持ち(2)、コンストラクターでこの値をセットするようにした(3)。
ShowAlertMessage
コンストラクターの : base(sender)
という部分を見ると分かるように、MvxMessage
クラス自体がコンストラクターにsenderというobject型の引数を持っている。これには送信元となるオブジェクトを指定すればよい。
メッセージの送信
メッセージの送信側となる部分の実装だが、まずIMvxMessenger
オブジェクトをIoCコンテナーから取得し、これを使ってPublishメソッドでメッセージを送信する。FirstViewModel.csファイルを開き、以下のように編集する。
using System;
using System.Threading.Tasks;
using Cirrious.MvvmCross.Plugins.Messenger;
using Cirrious.MvvmCross.ViewModels;
using MessengerSample.Core.Messages;
namespace MessengerSample.Core.ViewModels
{
public class FirstViewModel
: MvxViewModel
{
IMvxMessenger messenger;
public FirstViewModel(IMvxMessenger messenger)
{
this.messenger = messenger; // 1
}
MvxCommand showAlertCommand;
public MvxCommand ShowAlertCommand
{
get
{
return showAlertCommand ?? (showAlertCommand = new MvxCommand(async () => {
// 2
await Task.Delay(TimeSpan.FromSeconds(5));
messenger.Publish(new ShowAlertMessage(this, "Hello from Messenger!"));
}));
}
}
}
}
|
コンストラクターにIMvxMessenger
型の引数を持つことでIoCコンテナーからIMvxMessenger
オブジェクトを取得し、フィールドに保存しておく(1)。実際にメッセージを送る処理は、コマンドの中に記述した。Task.Delay
メソッドを使って5秒処理を待った上でメッセージを送信している(2)。メッセージの送信はIMvxMessenger.Publish<TMessage>
メソッドにMvxMessageを渡すことで行える。先ほど作ったShowAlertMessageを渡している。
iOSでのメッセージの受信とダイアログ表示
iOSでメッセージを受信してダイアログを表示する部分を実装する。MessengerSample.TouchプロジェクトのFirstView.csファイルを開き、以下のように編集する。
using Cirrious.CrossCore;
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Plugins.Messenger;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using MessengerSample.Core.Messages;
using ObjCRuntime;
using UIKit;
namespace MessengerSample.Touch.Views
{
[Register("FirstView")]
public class FirstView : MvxViewController
{
MvxSubscriptionToken showAlertSubscriptionToken;
public override void ViewDidLoad()
{
View = new UIView { BackgroundColor = UIColor.White };
base.ViewDidLoad();
// iOS 7 レイアウト
if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
{
EdgesForExtendedLayout = UIRectEdge.None;
}
var button = new UIButton(new CGRect(10, 90, 300, 40)); // 1
button.SetTitle("Show alert", UIControlState.Normal);
Add(button);
var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
set.Bind(button).To(vm => vm.ShowAlertCommand); // 2
set.Apply();
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
// 3
showAlertSubscriptionToken = Mvx.Resolve<IMvxMessenger>().SubscribeOnMainThread<ShowAlertMessage>(m =>
{
var alert = UIAlertController.Create("Message", m.AlertMessage, UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null); // 5
});
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
// 4
if (showAlertSubscriptionToken != null)
{
Mvx.Resolve<IMvxMessenger>().Unsubscribe<ShowAlertMessage>(showAlertSubscriptionToken);
showAlertSubscriptionToken = null;
}
}
}
}
|
まず、処理を発生させるボタンを用意し(1)、これにコマンドをバインディングする(2)。
次に、画面の準備が完了したら呼び出されるViewDidAppear
メソッドのタイミングでSubscriberを登録する(3)。Mvx.Resolve<T>
メソッドは、実行したその場でIoCコンテナーからオブジェクトを取得できる。取得したIMvxMessenger
オブジェクトのSubscribeOnMainThread<T>
メソッドで、ダイアログ表示の処理を記述する。なお、Subscribe<T>
メソッドでも受信は可能だが、このメソッドで登録した場合はPublisherと同じスレッドで実行されてしまう。UIの操作をメインスレッド以外で行うとクラッシュの要因となるため、受信した箇所でUIを操作するものは、メインスレッドでの実行が保証されるSubscribeOnMainThread<T>
メソッドを使用する。
どちらのSubscribe
系メソッドも、戻り値としてMvxSubscriptionToken
を返す。このサブスクリプショントークンは、Subscriberの登録解除の際に必要となるのでフィールド変数に保持しておく。画面を抜ける前に実行されるViewWillDisappear
メソッドで、先ほどのトークンを引数として渡してUnsubscribe<T>
メソッドを呼び出すことにより登録を解除する(4)。
実際の表示にはiOS標準のUIAlertControllerを使用している。UIAlertController.Create
メソッドでメッセージのタイトル、メッセージ内容、表示スタイルを指定してインスタンスを作り、[OK]ボタンをAddAction
メソッドで追加する。UIAlertContrllerはその名の通りUIViewControllerの派生クラスなので、PresentViewController
メソッドで表示する(5)。
Hint: ダイアログ表示をiOS 7以前に対応する場合
iOS 8でのダイアログ表示はUIAlertControllerを使用するが、iOS 7まではUIAlertViewを使用していた。UIAlertControllerはiOS 8で新規追加され、UIAlertViewは非推奨となった。iOS 7をターゲットとするアプリを開発する場合は、iOS 8以上はUIAlertController、iOS 8未満はUIAlertViewを使用する必要がある。OSのバージョン判定はUIDevice
クラスのCheckSystemVersion
メソッドを使うと簡単にできる。今回のサンプルでiOS 7に対応する場合は、以下のようにするとよい。
showAlertSubscriptionToken = Mvx.Resolve<IMvxMessenger>().SubscribeOnMainThread((ShowAlertMessage m) =>
{
if (UIDevice.CurrentDevice.CheckSystemVersion(8,0)) {
var alert = UIAlertController.Create("Message", m.AlertMessage, UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
}
else {
var alert = new UIAlertView("Message", m.AlertMessage, null, "OK");
alert.Show();
}
});
|
Androidでのメッセージの受信とダイアログ表示
Androidでも同様にダイアログを表示する実装をする。まず、MessengerSample.TouchプロジェクトのFirstView.axmlファイルを開き、以下のように編集する。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Show dialog"
local:MvxBind="Click ShowAlertCommand" /><!-- 1 -->
</LinearLayout>
|
ここでは、ShowAlertCommandを実行するボタンを作成した(1)。
次に、FirstView.csファイルを開き、以下のように編集する。
using Android.App;
using Android.OS;
using Cirrious.MvvmCross.Droid.Views;
using Cirrious.CrossCore;
using Cirrious.MvvmCross.Plugins.Messenger;
using MessengerSample.Core.Messages;
namespace MessengerSample.Droid.Views
{
[Activity(Label = "View for FirstViewModel")]
public class FirstView : MvxActivity
{
MvxSubscriptionToken showAlertSubscriptionToken;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.FirstView);
}
protected override void OnResume()
{
base.OnResume();
// 1
showAlertSubscriptionToken = Mvx.Resolve<IMvxMessenger>().SubscribeOnMainThread<ShowAlertMessage>(m =>
{
MessageDialogFragment.Create(m.AlertMessage).Show(FragmentManager, "1");
});
}
protected override void OnPause()
{
base.OnPause();
// 2
if (showAlertSubscriptionToken != null)
{
Mvx.Resolve<IMvxMessenger>().Unsubscribe<ShowAlertMessage>(showAlertSubscriptionToken);
showAlertSubscriptionToken = null;
}
}
private class MessageDialogFragment : DialogFragment {
private const string MessageKey = "Message";
// 3
public static MessageDialogFragment Create(string message)
{
var bundle = new Bundle();
bundle.PutString(MessageKey, message);
return new MessageDialogFragment
{
Arguments = bundle
};
}
public override Dialog OnCreateDialog(Bundle savedInstanceState)
{
// 4
return new AlertDialog.Builder(Activity)
.SetTitle("Message")
.SetMessage(this.Arguments.GetString(MessageKey))
.SetPositiveButton("OK", (Android.Content.IDialogInterfaceOnClickListener)null)
.Create();
}
}
}
}
|
iOSのときと同様、画面表示時に実行されるOnResume
メソッドでSubscribeを登録し(1)、画面から抜ける際に実行されるOnPause
メソッドでその登録を解除する(2)。
実際のダイアログの表示は、DialogFragment
クラスを派生させたクラスを実装する必要がある。Fragmentの外から値を引き渡すときは、基底クラスにあるArguments
プロパティにBundleをセットするようにするが、この処理をCreate
メソッドにまとめておくと、その後で引数が増えたときに、そのメソッド内を修正するのみで済むため、都合がよい。今回はDialogFragment
の派生クラスMessageDialogFragment
に静的なCreate
メソッドを定義して実現した(3)。
ダイアログの作成はOnCreateDialog
メソッドからDialog
オブジェクトを返す(4)。
実行結果の確認
このアプリを実行すると、画面上にボタンが表示され、ボタンをタップして5秒後にメッセージダイアログが表示される。
乱用に注意する
Messengerなどのイベント通知機構は便利な半面、送ったメッセージをどこで使用しているかを追いづらくなる弊害がある。例えば、同じメッセージに対して違う場所から別々の処理が登録されていて、その処理が相互に作用を起こす場合だ。この場合、片方にデバッガーでブレークポイントを指定して動作を見ていても、もう片方にはブレークがかからず、問題の解決に手間を要することになる。
Messengerを使う前にプラグインやPCL(ポータブルクラスライブラリ)が用意されているライブラリで代用できないか検討し、使用する際はSubscriberを登録する場所と解除する場所をよく検討した上で利用したい。
※以下では、本稿の前後を合わせて5回分(第57回~第61回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
57. MvvmCrossで画像をバインディングするには?
MvvmCrossでのiOS/Androidアプリ開発において、画像のURLをViewへバインディングできるMvxImageViewの使い方を説明する。
58. MvvmCrossで文字列をローカライズ(多言語化)するには?
MvvmCrossでのiOS/Androidアプリ開発において、ViewModelの文字列リソースを多言語化してローカライズする方法を解説する。
59. 【現在、表示中】≫ MvvmCrossでViewModelからViewにイベントを通知するには?(Messengerパターン)
MvvmCrossでのiOS/Androidアプリ開発において、ViewModelからViewにイベントを通知するMessengerパターンの実装方法を紹介する。
60. XamarinのUIやコードの実行結果を簡単に確認できる「Sketches」を使うには?
Xamarin.Formsのレイアウトなどを、ビルドすることなくREPL環境で手軽に確認できるSketchesの使い方を紹介する。