MVVM

MVVM Light Messenger の詳細

Laurent Bugnion

モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンと MVVM Light Toolkit を取り上げるこのシリーズは約 1 年前に連載を開始し、以来、MVVM アプリにおける IOC コンテナーの使用方法、スレッド間アクセスや MVVM Light の DispatcherHelper コンポーネントの処理方法など、たくさんの問題を扱ってきました。また、コマンド処理 (RelayCommand や EventToCommand)、ビュー サービス (Navigation サービスや Dialog サービスなど)、Messenger コンポーネントの簡単な説明なども紹介しました。

Messenger コンポーネントは、実は MVVM Light Toolkit の非常に強力な要素の 1 つで、使いやすく、多くの開発者にとって魅力的なコンポーネントですが、誤用するとリスクが伴うため論争の火種にもなっています。そこで今回は、このコンポーネントのしくみ、起こり得るリスク、このコンポーネントが効果的なシナリオなどをまとめてみることにしました。

ここでは、Messenger の実装を支える一般原理について説明し、従来の手法に比べてこの実装が使いやすい理由も紹介します。また、特定の予防措置を講じない場合に、このアプローチがメモリに与える影響についても説明します。最後に、MVVM Light Messenger 自体の詳細について、特に、組み込みのメッセージとその使用方法に重点を置いて説明します。

イベント アグリゲーションと Messenger を簡単にする

Messenger のようなシステムを、イベント バスやイベント アグリゲーターとも呼びます。このようなコンポーネントは、送信側 (パブリッシャー) と受信側 (サブスクライバー) を接続します。MVVM Light を作成したとき、多くのメッセージング システムでは、具体的なメソッドを実装するために受信側または送信側が必要でした。たとえば、Receive メソッドを指定する IReceiver インターフェイスがあるとした場合、このメッセージング システムで登録を行うオブジェクトはこのインターフェイスを実装しなければなりませんでした。このような制約は、このメッセージング システムを実際に使用する開発者に制限を課すためわずらわしいものでした。たとえば、サードパーティのアセンブリを使用する場合、そのアセンブリのコードにアクセスする権限がなければ、IReceiver を実装するようにサードパーティのクラスを変更できないため、このメッセージング システムを使用してこのライブラリからインスタンスを登録することはできません。

MVVM Light Messenger を作成した目的は、受信側、送信側、メッセージに任意のオブジェクトを使用できるというシンプルな前提を用意して、こうしたシナリオの効率を上げることでした。

同時に用語も簡単にしました。定義がわかりにくい「イベント アグリゲーション」のような用語ではなく、伝えたい内容がメッセージングであることが明確に分かる用語を使用します。「サブスクライバー」を「受信側」、「パブリッシャー」を「送信側」と呼び、「イベント」の代わりに「メッセージ」を使用します。このような用語の簡略化は実装の簡略化と並行して行われ、Messenger を使いやすく、そのしくみを理解しやすいものにしています。

たとえば、図 1 のコードを考えてみます。ここでは 2 つの別のオブジェクトで MVVM Light Messenger を使用しています。Registration オブジェクトは、すべての RegisteredUser インターフェイスにメッセージを送信します。この種のシナリオは複数の方法で実装できるため、Messenger が必ずしも最善策とは限りません。しかし、使用しているアーキテクチャによっては、特に、送信側と受信側がアプリの一部を構成し、両者を分離した状態を保つ場合には、Messenger がこの機能を実装する非常に優れたツールになります。Registration インスタンスは RegisteredUser のインスタンスに明示的に送信していません。代わりに、Messenger を使用してメッセージをブロードキャストします。任意のインスタンスが、この種のメッセージを登録でき、メッセージの送信時に通知を受け取ることができます。この例では、送信するメッセージは RegistrationInfo のインスタンスです。ただし、単純な値 (int 値、ブール値など) から専用のメッセージ オブジェクトまで、あらゆる種類のメッセージを送信できます。後ほど、メッセージの使用方法と、MVVM Light 組み込みのメッセージの種類をいくつか紹介します。

図 1 メッセージの送受信

public class Registration
{
  public void SendUpdate()
  {
    var info = new RegistrationInfo
    {
      // ... Some properties
    };
    Messenger.Default.Send(info);
  }
}
public class RegisteredUser
{
  public RegisteredUser()
  {
    Messenger.Default.Register<RegistrationInfo>(
      this,
      HandleRegistrationInfo);
  }
  private void HandleRegistrationInfo(RegistrationInfo info)
  {
    // Update registered user info
  }
}
public class RegistrationInfo
{
  // ... Some properties
}

図 1 のコードでは、メッセージの種類 (RegistrationInfo) の登録がデリゲート (HandleRegistrationInfo) によって行われます。これは Microsoft .NET Framework でよく使うメカニズムです。たとえば、C# でイベント ハンドラーを登録する場合にも、名前付きメソッドまたは匿名ラムダ式のいずれかでイベントにデリゲートを渡します。同様に、名前付きメソッドまたは匿名ラムダ式を使用して、Messenger で受信側を登録することも可能です (図 2 参照)。

図 2 名前付きメソッドまたはラムダ式による登録

public UserControl()
{
  InitializeComponent();
  // Registering with named methods ----
  Loaded += Figure2ControlLoaded;
  Messenger.Default.Register<AnyMessage>(
    this,
    HandleAnyMessage);
  // Registering with anonymous lambdas ----
  Loaded += (s, e) =>
  {
    // Do something
  };
  Messenger.Default.Register<AnyMessage>(
    this,
    message =>
    {
      // Do something
    });
}
private void HandleAnyMessage(AnyMessage message)
{
  // Do something
}
private void Figure2ControlLoaded (object sender, RoutedEventArgs e)
{
  // Do something
}

スレッド間アクセス

Messenger は、メッセージの送信先スレッドを監視しません。前回の「MVVM アプリにおけるマルチスレッドとディスパッチ」 (msdn.microsoft.com/ja-jp/magazine/dn630646.aspx) で説明したように、あるスレッドで実行中のオブジェクトから別のスレッドに属しているオブジェクトにアクセスを試みる場合、いくつか予防措置を講じる必要があります。この問題は、バックグラウンド スレッドと、UI スレッドに属するコントロールとの間で発生することがよくあります。前回は、MVVM Light DispatcherHelper を使用して UI スレッドの操作を "ディスパッチ" する方法と、スレッド間アクセスの例外を回避する方法を紹介しました。

イベント アグリゲーターの中には、UI スレッドに送信されるメッセージを自動的にディスパッチできるようにするものもあります。ただし、MVVM Light Messenger は、Messenger API の簡略化を目的としていているため、このようなディスパッチは行いません。UI スレッドにメッセージを自動的にディスパッチするオプションを追加すると、登録メソッドのパラメーターが増えることになります。さらに、ディスパッチが明示的でなくなり、経験の浅い開発者が内部の処理を理解するのが難しくなる可能性があります。

代わりに、開発者が必要に応じて UI スレッドにメッセージを明示的にディスパッチします。この場合、MVVM Light DispatcherHelper を使用するのが最善の方法です。前回示したように、CheckBeginInvokeOnUI メソッドは必要な場合にのみ操作をディスパッチします。UI スレッドで Messenger が既に実行されている場合、メッセージをディスパッチしなくてもすぐに配布できます。

public void RunOnBackgroundThread()
{
  // Do some background operation
  DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
      Messenger.Default.Send(new ConfirmationMessage());
    });
}

メモリ処理

オブジェクトが相互に認識しなくても通信できるようにするシステムでは、メッセージの受信者への参照を保存しなくてはならないという困難な状況に直面します。たとえば、.NET イベント処理システムを考えると、このシステムではイベントを発生させるオブジェクトと、そのイベントをサブスクライブするオブジェクトの間に強い参照を作成します。図 3 のコードでは、_first と _second の間に強いリンクを作成します。このため、CleanUp メソッドを呼び出して _second を null に設定しても、_first は依然として _second への参照を保持しているため、ガベージ コレクターは _second をメモリから削除できません。ガベージ コレクターは、オブジェクトをメモリから削除できるかどうかを判断する際、オブジェクトの参照カウントを利用します。しかし、Second インスタンスではこの処理が行われないためメモリ リークが発生します。このメモリ リークが原因で、時間の経過と共に多くの問題が発生する可能性があります。アプリの速度は大幅に低下し、最終的にはクラッシュすることもあります。

図 3 インスタンス間の強い参照

public class Setup
{
  private First _first = new First();
  private Second _second = new Second();
  public void InitializeObjects()
  {
    _first.AddRelationTo(_second);
  }
  public void Cleanup()
  {
    _second = null;
    // Even though this is set to null, the Second instance is
    // still kept in memory because the reference count isn't
    // zero (there's still a reference in _first).
  }
}
public class First
{
  private object _another;
  public void AddRelationTo(object another)
  {
    _another = another;
  }
}
public class Second
{
}

この問題を軽減するため、.NET の開発者は WeakReference オブジェクトを思い付きました。このクラスは、オブジェクトへの参照を "弱い" 参照方法で格納することができるようにします。そのオブジェクトの他の参照がすべて null に設定された場合、そのオブジェクトを使用する WeakReference オブジェクトがあっても、ガベージ コレクターはそのオブジェクトのガベージ コレクションを実行できます。この考え方は非常に便利で、必ずしもすべての問題を解決できるわけではありませんが、適切に使用すればメモリ リークの問題を軽減できます。これを説明するために、図 4 では SimpleMessenger オブジェクトが Receiver への参照を WeakReference 方式で格納する簡単な通信システムを示します。この例では、メッセージを処理する前に、IsAlive プロパティをチェックしています。Receiver が既に削除され、以前にガベージ コレクションが行われていると、IsAlive プロパティは false になります。このことは、WeakReference がもはや無効になっていて、削除すべきことを示します。

図 4 WeakReference インスタンスの使用

public class SuperSimpleMessenger
{
  private readonly List<WeakReference> _receivers
    = new List<WeakReference>();
  public void Register(IReceiver receiver)
  {
    _receivers.Add(new WeakReference(receiver));
  }
  public void Send(object message)
  {
    // Locking the receivers to avoid multithreaded issues.
    lock (_receivers)
    {
      var toRemove = new List<WeakReference>();
      foreach (var reference in _receivers.ToList())
      {
        if (reference.IsAlive)
        {
          ((IReceiver)reference.Target).Receive(message);
        }
        else
        {
          toRemove.Add(reference);
        }
      }
      // Prune dead references.
      // Do this in another loop to avoid an exception
      // when modifying a collection currently iterated.
      foreach (var dead in toRemove)
      {
        _receivers.Remove(dead);
      }
    }
  }
}

MVVM Light Messenger はほぼこれと同じ原理に基づい構築されていますが、当然、複雑な面もたくさんあります。特に、Messenger は任意のインターフェイスを実装するために Receiver を必要としないので、メッセージの送信に使用するメソッド (コールバック) への参照を格納する必要があります。このことは、Windows Presentation Foundation (WPF) や Windows Runtime では問題ありません。しかし、Silverlight や Windows Phone ではフレームワークのセキュリティが厳しいため、API によって特定の操作の実行が妨げられます。特定の場合に、このような制限の 1 つによって Messenger システムが影響を受けます。

このことを理解するには、メッセージを処理するために登録できるメソッドの種類を知っておく必要があります。まとめると、受信のメソッドは静的メソッドにすることができるため問題にはなりません。つまり、このメソッドをインスタンス メソッドすることができ、そうすればパブリック、内部、プライベートの各メソッドを区別できます。ほとんどの場合、受信のメソッドは匿名ラムダ式にします。これは、プライベート メソッドと同じです。

静的メソッドまたはパブリック メソッドにすると、メモリ リークが発生するリスクがなくなります。処理するメソッドを内部メソッドまたはプライベート メソッド (匿名ラムダ式) にすると、Silverlight と Windows Phone ではリスクを生じる可能性があります。残念ながらこのような場合、Messenger 用に WeakReference を使用する方法はありません。繰り返しになりますが、WPF や Windows Runtime では問題になりません。ここまでの説明を図 5 にまとめます。

図 5 登録解除の失敗によるメモリ リークのリスク

可視性 WPF Silverlight Windows Phone 8 Windows Runtime
静的 リスクなし リスクなし リスクなし リスクなし
パブリック リスクなし リスクなし リスクなし リスクなし
内部 リスクなし リスクあり リスクあり リスクなし
プライベート リスクなし リスクあり リスクあり リスクなし
匿名ラムダ式 リスクなし リスクあり リスクあり リスクなし

図 5 に示すように、リスクがある場合でも、登録解除の失敗により必ずしもメモリ リークが発生するわけではないことに注意してください。つまり、メモリ リークを発生しないようにするには、受信側を必要としなくなったら、Messenger から受信側の登録解除を明示的に行うようにします。この処理は、Unregister メソッドを使用して実行できます。Unregister メソッドには複数のオーバーロードがあります。受信側は Messenger から完全に登録解除できます。また、特定の 1 つのメソッドのみ登録解除して、他のメソッドはアクティブな状態を保持することもできます。

Messenger 使用時のその他のリスク

前述のとおり、MVVM Light Messenger は非常に強力で汎用性の高いコンポーネントですが、使用時にはいくつかリスクも伴います。Silverlight や Windows Phone でメモリ リークが発生する可能性があることは既に説明しました。技術面以外にもリスクがあります。Messenger を使用するとオブジェクトが分離されるため、メッセージの送受信時に何が起こっているかを正確に把握するのが難しくなります。これまでイベント バスを使用したことがない、経験の浅い開発者にとっては、操作の流れをたどるのが難しくなるかもしれません。たとえば、メソッドの呼び出しにステップ インしているときに、このメソッドが Messenger.Send メソッドを呼び出すとすると、対応する Messenger.Receive メソッドを検索し、そのメソッドの場所にブレークポイントを設定することを知らなければ、デバッグの流れを見失います。ただし、Messenger の操作は同期操作なので、Messenger のしくみを理解すれば、この流れをデバッグできます。

個人的には、Messenger を「最後の手段」として使用します。たとえば、もっと便利なプログラミング手法を使用できない場合や、そのプログラミング手法を使用すると、できる限り分離する必要があるアプリの各部の間でさまざまな依存関係が発生する場合などに使用します。ただし、IOC コンテナーや、より明確な方法で同じ効果が得られるサービスなど、他のツールを使用する方が好ましい場合もあります。IOC とビュー サービスについては、このシリーズの初回の記事 (bit.ly/1m9HTBX、英語) で説明しています。

1 つまたは複数の Messenger

MVVM Light Messenger などのメッセージング システムの利点の 1 つは、このようなシステムを複数のアセンブリにまたがって使用できることです (プラグイン シナリオなど)。これは、大規模アプリの構築、特に WPF では一般的なアーキテクチャです。ただし、プラグイン システムは小さなアプリでも役に立ちます。たとえば、主要部分を再コンパイルする必要なく、新しい機能を簡単に追加できます。DLL がアプリの AppDomain に読み込まれた直後から、その DLL に含まれるクラスは MVVM Light Messenger を使用して同じアプリの他のコンポーネントと通信できるようになります。これは、特に、メイン アプリが、読み込まれるサブ コンポーネントの数を把握していないとき、つまり一般的にはプラグイン ベースのアプリの場合には非常に効果的です。

通常、アプリが必要とするのは、すべての通信に対応する Messenger インスタンス 1 つだけです。おそらく、Messenger.Default プロパティに格納される静的インスタンスだけが必要です。ただし、必要であれば Messenger の新しいインスタンスを作成できます。複数のインスタンスを作成すると、各インスタンスは独立した通信チャネルとして機能します。これは、特定のオブジェクトがそのオブジェクトを対象としていないメッセージを受け取らないようにする場合に便利です。たとえば、図 6 のコードでは、2 つのクラスで同じメッセージの種類を登録しています。メッセージを受け取るときは、両方のインスタンスがいくつかチェックを行ってメッセージの内容を把握する必要があります。

図 6 既定の Messenger の使用と送信側のチェック

 

public class FirstViewModel
{
  public FirstViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      message =>
      {
        if (message.Sender is MainViewModel)
        {
          // This message is for me.
        }
      });
  }
}
public class SecondViewModel
{
  public SecondViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      message =>
      {
        if (message.Sender is SettingsViewModel)
        {
          // This message is for me
        }
      });
  }
}

図 7 に、プライベート Messenger インスタンスの実装を示します。この場合、SecondViewModel は、Messenger の異なるインスタンスをサブスクライブし、異なるチャネルをリッスンするため、メッセージを受け取ることはありません。

図 7 プライベート Messenger の使用

public class MainViewModel
{
  private Messenger _privateMessenger;
  public MainViewModel()
  {
    _privateMessenger = new Messenger();
    SimpleIoc.Default.Register(() => _privateMessenger, 
      "PrivateMessenger");
  }
  public void Update()
  {
    _privateMessenger.Send(new NotificationMessage("DoSomething"));
  }
}
public class FirstViewModel
{
  public FirstViewModel()
  {
    var messenger
      = SimpleIoc.Default.GetInstance<Messenger>("PrivateMessenger");
    messenger.Register<NotificationMessage>(
      this,
      message =>
      {
        // This message is for me.
      });
  }
}

任意のメッセージを特定の受信側に送信しないようにするもう 1 つの方法では、トークンを使用します (図 8 参照)。これは、送信側と受信側の間のある種の契約です。通常、トークンは GUID などの一意識別子ですが、任意のオブジェクトにすることもできます。送信側と受信側で同じトークンを使用すると、この 2 つのオブジェクトの間でプライベート通信チャネルが開かれます。このシナリオでは、トークンを使用しなかった SecondViewModel にはメッセージの着信が通知されません。この手法の主な利点は、受信側でロジックを作成して、メッセージが実際にその受信側を対象としていることを確認する必要がないことです。代わりに、Messenger がトークンに基づいてメッセージをフィルター処理します。

図 8 トークンによって区別される通信チャネル

public class MainViewModel
{
  public static readonly Guid Token = Guid.NewGuid();
  public void Update()
  {
    Messenger.Default.Send(new NotificationMessage("DoSomething"),
      Token);
  }
}
public class FirstViewModel
{
  public FirstViewModel()
  {
    Messenger.Default.Register<NotificationMessage>(
      this,
      MainViewModel.Token,
      message =>
      {
        // This message is for me.
      });
  }
}

メッセージを使用する

メッセージをフィルターするにはトークンを使用するのが優れた方法ですが、メッセージの内容を相手に認識してもらうために、メッセージにコンテキスト情報を含めて送信しなければならないという状況は変わりません。たとえば、ブール値のコンテンツを指定して Send メソッドと Receive メソッドを使用することができます (図 9 参照)。ただし、複数の送信側がブール値のメッセージを送信すると、受信側はメッセージの対象や目的を推測するのが難しくなります。このような場合は、状況を明確にするために、専用のメッセージの種類を使用するのが適切です。

図 9 メッセージの種類を使用したコンテキストの定義

public class Sender
{
  public void SendBoolean()
  {
    Messenger.Default.Send(true);
  }
  public void SendNotification()
  {
    Messenger.Default.Send(
      new NotificationMessage<bool>(true, Notifications.PlayPause));
  }
}
public class Receiver
{
  public Receiver()
  {
    Messenger.Default.Register<bool>(
      this,
      b =>
      {
        // Not quite sure what to do with this boolean.
      });
    Messenger.Default.Register<NotificationMessage<bool>>(
      this,
      message =>
      {
        if (message.Notification == Notifications.PlayPause)
        {
          // Do something with message.Content.
          Debug.WriteLine(message.Notification + ":" + 
            message.Content);
        }
      });
  }
}

図 9 では、使用している具体的なメッセージの種類も示しています。NotificationMessage<T> は MVVM Light Toolkit に組み込まれていて、最もよく使用されるメッセージの種類の 1 つです。このメッセージの種類では、通知文字列と共に任意のコンテンツ (この場合はブール値) を送信できます。通常、通知は Notifications という静的クラスに定義される一意文字列で、メッセージと共に指示を送信できます。

当然、NotificationMessage<T> からの派生、別の組み込みメッセージの種類の使用、独自のメッセージの種類の実装も可能です。MVVM Light Toolkit には、このような目的に派生できる MessageBase クラスがあります。ただし、必ずしもコードで使用する義務はありません。

もう 1 つ、PropertyChanged­Message<T> という組み込みのメッセージの種類があります。これは、通常、バインド操作に関係するオブジェクトの基本クラスとして使用される ObservableObject クラスや ViewModelBase クラスに関連して特に役立ちます。これらのクラスは、データ バインドを使用する MVVM アプリで重要になる、INotifyPropertyChanged インターフェイスの実装です。たとえば、図 10 のコードでは、BankAccountViewModel が Balance という監視可能なプロパティを定義します。このプロパティが変化すると、RaisePropertyChanged メソッドがブール値のパラメーターを受け取ります。これにより、ViewModelBase クラスがこのプロパティに関する情報 (名前、古い値、新しい値など) を含む PropertyChangedMessage をブロードキャストします。別のオブジェクトはこのメッセージの種類をサブスクライブして、変化に対処することができます。

図 10 PropertyChangedMessage の送信

public class BankViewModel : ViewModelBase
{
  public const string BalancePropertyName = "Balance";
  private double _balance;
  public double Balance
  {
    get
    {
      return _balance;
    }
    set
    {
      if (Math.Abs(_balance - value) < 0.001)
      {
        return;
      }
      var oldValue = _balance;
      _balance = value;
      RaisePropertyChanged(BalancePropertyName, oldValue, value, true);
    }
  }
}
public class Receiver
{
  public Receiver()
  {
    Messenger.Default.Register<PropertyChangedMessage<double>>(
      this,
      message =>
      {
        if (message.PropertyName == BankViewModel.BalancePropertyName)
        {
          Debug.WriteLine(
            message.OldValue + " --> " + message.NewValue);
        }
      });
  }
}

MVVM Light には、ほかにもさまざまなシナリオで役に立つ組み込みメッセージがあります。また、独自のカスタム メッセージを構築するインフラストラクチャも利用できます。要するに、このようなメッセージの種類は、メッセージの内容に対する対処方法を把握するための十分なコンテキストを提供し、受信側の処理を簡単にすることを目的としています。

まとめ

Messenger は、完全に分離したメッセージング ソリューションがなければ実装が難しくなるような多くのシナリオで非常に役立つことが実証されています。ただし、これは高度なツールなので、デバッグやメンテナンスが難しくなる複雑なコードを作成しないように注意する必要があります。

MVVM Light Toolkit コンポーネントの紹介については今回で終了です。XAML ベースの複数のプラットフォームで同じツールやテクニックを使用できる .NET 開発者の方にとって有意義な内容になったでしょうか。MVVM Light を使用すると、WPF、Windows Runtime、Windows Phone、Silverlight、そして Android や iOS の Xamarin プラットフォームでもコードを共有できます。アプリの効率的な開発にどのように MVVM Light が役に立つかを理解し、このようなアプリの設計、テスト、およびメンテナンスを簡単にするのに、このシリーズがお役に立てばさいわいです。

Laurent Bugnion は、Windows Presentation Foundation、Silverlight、Pixelsense、Kinect、 Windows 8、Windows Phone、UX などのテクノロジーに携わる Microsoft パートナーの IdentityMine Inc で、シニア ディレクターを務めています。彼は、スイスのチューリッヒを拠点に活動しているMicrosoft MVP および Microsoft Regional Director です。

この記事のレビューに協力してくれた技術スタッフの Jeffrey Ferman に心より感謝いたします。
Jeffrey Ferman は、現在、Visual Studio のプログラム マネージャーを務めています。これまで 4 年間、Visual Studio と Blend の両方で XAML ツールを中心に取り組んでいます。彼は、基幹業務アプリの構築やさまざまなデザイン パターンやプラクティスの利用を楽しんでいます。また、拡張性に情熱を燃やしていて、顧客と共同でコントロールのデザイン時エクスペリエンスを構築する作業も楽しんでいます。