Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossでViewModelからViewにイベントを通知するには?(Messengerパターン)

2015年9月16日

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)。

図1 Messengerの動作
図1 Messengerの動作

まず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ファイルの内容を以下のように編集する。

C#
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;
    }
  }
}
リスト1 メッセージの定義(ShowAlertMessage.cs)

 メッセージはMvxMessageクラスから派生する必要がある(1)。メッセージクラスには、このメッセージで渡したい情報を、プロパティとして持たせておくこともできる。今回は表示するテキストをAlertMessageプロパティとして持ち(2)、コンストラクターでこの値をセットするようにした(3)。

 ShowAlertMessageコンストラクターの : base(sender)という部分を見ると分かるように、MvxMessageクラス自体がコンストラクターにsenderというobject型の引数を持っている。これには送信元となるオブジェクトを指定すればよい。

メッセージの送信

 メッセージの送信側となる部分の実装だが、まずIMvxMessengerオブジェクトをIoCコンテナーから取得し、これを使ってPublishメソッドでメッセージを送信する。FirstViewModel.csファイルを開き、以下のように編集する。

C#
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!"));
        }));
      }
    }
  }
}
リスト2 メッセージの送信(FirstViewModel)

 コンストラクターにIMvxMessenger型の引数を持つことでIoCコンテナーからIMvxMessengerオブジェクトを取得し、フィールドに保存しておく(1)。実際にメッセージを送る処理は、コマンドの中に記述した。Task.Delayメソッドを使って5秒処理を待った上でメッセージを送信している(2)。メッセージの送信はIMvxMessenger.Publish<TMessage>メソッドにMvxMessageを渡すことで行える。先ほど作ったShowAlertMessageを渡している。

iOSでのメッセージの受信とダイアログ表示

 iOSでメッセージを受信してダイアログを表示する部分を実装する。MessengerSample.TouchプロジェクトのFirstView.csファイルを開き、以下のように編集する。

C#
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;
      }
    }
  }
}
リスト3 iOSでのメッセージの受信とダイアログ表示(FirstView.cs)

 まず、処理を発生させるボタンを用意し(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に対応する場合は、以下のようにするとよい。

C#
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();
  }
  });
リスト4 ダイアログ表示をiOS 7以前に対応する場合のコード例

Androidでのメッセージの受信とダイアログ表示

 Androidでも同様にダイアログを表示する実装をする。まず、MessengerSample.TouchプロジェクトのFirstView.axmlファイルを開き、以下のように編集する。

XML
<?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>
リスト5 ダイアログを表示するためのボタンの定義(FirstView.axml)

 ここでは、ShowAlertCommandを実行するボタンを作成した(1)。

 次に、FirstView.csファイルを開き、以下のように編集する。

C#
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();
      }
    }
  }
}
リスト6 Androidでのメッセージの受信とダイアログ表示(FirstView.cs)

 iOSのときと同様、画面表示時に実行されるOnResumeメソッドでSubscribeを登録し(1)、画面から抜ける際に実行されるOnPauseメソッドでその登録を解除する(2)。

 実際のダイアログの表示は、DialogFragmentクラスを派生させたクラスを実装する必要がある。Fragmentの外から値を引き渡すときは、基底クラスにあるArgumentsプロパティにBundleをセットするようにするが、この処理をCreateメソッドにまとめておくと、その後で引数が増えたときに、そのメソッド内を修正するのみで済むため、都合がよい。今回はDialogFragmentの派生クラスMessageDialogFragmentに静的なCreateメソッドを定義して実現した(3)。

 ダイアログの作成はOnCreateDialogメソッドからDialogオブジェクトを返す(4)。

実行結果の確認

 このアプリを実行すると、画面上にボタンが表示され、ボタンをタップして5秒後にメッセージダイアログが表示される。

図2 ダイアログの表示

ボタンをタップして5秒後にダイアログが表示される。メッセージの内容はViewModel内で指定したものになっていることが分かる。

乱用に注意する

 Messengerなどのイベント通知機構は便利な半面、送ったメッセージをどこで使用しているかを追いづらくなる弊害がある。例えば、同じメッセージに対して違う場所から別々の処理が登録されていて、その処理が相互に作用を起こす場合だ。この場合、片方にデバッガーでブレークポイントを指定して動作を見ていても、もう片方にはブレークがかからず、問題の解決に手間を要することになる。

 Messengerを使う前にプラグインやPCL(ポータブルクラスライブラリ)が用意されているライブラリで代用できないか検討し、使用する際はSubscriberを登録する場所と解除する場所をよく検討した上で利用したい。

※以下では、本稿の前後を合わせて5回分(第57回~第61回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Xamarin逆引きTips
57. MvvmCrossで画像をバインディングするには?

MvvmCrossでのiOS/Androidアプリ開発において、画像のURLをViewへバインディングできるMvxImageViewの使い方を説明する。

Xamarin逆引きTips
58. MvvmCrossで文字列をローカライズ(多言語化)するには?

MvvmCrossでのiOS/Androidアプリ開発において、ViewModelの文字列リソースを多言語化してローカライズする方法を解説する。

Xamarin逆引きTips
59. 【現在、表示中】≫ MvvmCrossでViewModelからViewにイベントを通知するには?(Messengerパターン)

MvvmCrossでのiOS/Androidアプリ開発において、ViewModelからViewにイベントを通知するMessengerパターンの実装方法を紹介する。

Xamarin逆引きTips
60. XamarinのUIやコードの実行結果を簡単に確認できる「Sketches」を使うには?

Xamarin.Formsのレイアウトなどを、ビルドすることなくREPL環境で手軽に確認できるSketchesの使い方を紹介する。

Xamarin逆引きTips
61. Xamarin Android Playerを使うには?

Xamarin製の高性能Android EmulatorであるXamarin Android Playerの使い方を紹介する。

サイトからのお知らせ

Twitterでつぶやこう!