sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# CommunityToolkit.Mvvm の学習7 Messenger

Messenger

IMessenger インターフェースは、異なるオブジェクト間でメッセージを交換するために使用できる型の契約です。これは、参照される型への強参照を保持することなく、アプリケーションの異なるモジュールとモジュールを分離するために便利です。また、トークンで一意に識別される特定のチャンネルにメッセージを送信したり、アプリケーションの異なるセクションで異なる Messenger を持つことも可能です。

MVVM Toolkit は2つの実装を提供します。WeakReferenceMessengerStrongReferenceMessenger です。

WeakReferenceMessenger は、内部的に弱参照を使用して、受信者 (recipient) に自動的なメモリ管理を提供しています。StrongReferenceMessenger は、強参照を使用して、不要になったときに開発者がマニュアルで受信者の登録を解除する必要があります。(登録の解除方法についての詳細は後述)しかし、その代わりにパフォーマンスが(両者の比較として)向上して、メモリ使用量がずっと少なくなります。

APIs:

  • Messenger
  • WeakReferenceMessenger
  • StrongReferenceMessenger
  • IRecipient
  • MessageHandler<TRecipient, TMessage>
  • ObservableRecipient
  • RequestMessage
  • AsyncRequestMessage
  • CollectionRequestMessage
  • AsyncCollectionRequestMessage

どのように機能するか (How it works)

IMessenger を実装した型は、受信者(メッセージの受信者)とその登録をしたメッセージの型との間のリンクを、相対的なメッセージハンドラを使って維持しています。

どのオブジェクトでも、メッセージハンドラを使用して、与えられたメッセージの型の受信者として登録することができます。IMessenger インスタンスが、(なにかの型の)メッセージを送信するために使用する毎に呼び出されます。

また、複数のモジュールが衝突を起こすことなく同じ型のメッセージを交換できるように、特定の通信チャンネル(それぞれ一意のトークンによって式ベルされるもの)を介してメッセージを送信することも可能です。トークンなしで送信されたメッセージは、デフォルトの共用チャンネルを使用しています。

Messenger は IRecipient<TMessage> インターフェースを使う方法と MessageHandler<TRecipient, TMessage> デリゲートをメッセージハンドラとして使う方法があります。前者は RegisterAll エクステンションを1回呼び出すだけですべてのハンドラを登録することができます。宣言されたすべてのメッセージハンドラの受信者が自動的に登録されます。後者は、より柔軟性が必要な場合、または、単純なラムダ式をメッセージハンドラとして使用する場合に役立ちます。

WeakReferenceMessengerStrongReferenceMessenger は、パッケージに組み込まれたスレッドセーフな実装の Default プロパティも公開しています。必要であれば、複数の Messenger インスタンスも作成することも可能です。例えば、アプリケーションの異なるモジュール(同じプロセスで実行されている複数のウィンドウ)に DI サービスプロバイダで異なる Messenger を挿入する場合などです。

Note: WeakReferenceMessenger は使いやすく、また MvvmLight ライブラリの Messenger の同じ動作をするため、MVVM Toolkit の ObservableRecipient 型でも使用されるデフォルトの型になります。

送信 - 送信と受信メッセージ (Sending messages - Sending and receiving messages)

例を挙げます:

// Create a message
public class LoggedInUserChangedMessage : ValueChangedMessage<User>
{
    public LoggedInUserChangedMessage(User user) : base(user)
    {        
    }
}

// Register a message in some module
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this, (r, m) =>
{
    // ここでメッセージを処理します。引数の r は受信者で m は入力メッセージです。
    // 入力として渡された受信者を使用することで、ラムダ式は "this" をキャプチャしないので、パフォーマンスが向上します。
});

// Send a message from some other module
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));

このメッセージタイプが単純な Messaging アプリケーションで使用されて、現在のログインしているユーザーのユーザー名とプロフィール画像が表示されているヘッダー、会話リストが表示されているパネル、そして、現在の会話からメッセージが選択されている場合は、別のパネルが表示されている例を想像してみてください。

ここでは3つの viewmodel のケースとして、それぞれ HeaderViewModelConversationsListViewModelConversationViewModel によって(例を)実装されているとします。

このシナリオの例では、ログイン操作が完了した後、HeaderViewModel から LoggedInUserChangedMessage メッセージが送信されます。他の viewmodel は、それに対してハンドラを登録するとよいでしょう。例えば、ConversationsListViewModel は新しいユーザーの会話のリストを読み込み、ConversationViewModel は現在の会話が存在する場合は、それを閉じます。

IMessenger インスタンスは、登録されたすべての受信者にメッセージを配信します。受信者は特定の型のメッセージを購読できるようになっている点に注意してください。MVVM Toolkit によって提供されるデフォルトの IMessenger の実装できは、継承されたメッセージの型は登録されません。

受信者はメッセージの受信が不要になったとき、登録を解除してメッセージの受信を停止します。登録解除の方法は、メッセージの型、登録トークン、受信者ごとに解除できます:

// Unregisters the recipient from a message type
// メッセージの型から解除
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage>(this);

// Unregisters the recipient from a message type in a specified channel
// 指定したチャンネルのメッセージ型から解除
WeakReferenceMessenger.Default.Unregister<LoggedInUserChangedMessage, int>(this, 42);

// Unregister the recipient from all messages, across all channels
// すべてのチャンネル/メッセージから解除
WeakReferenceMessenger.Default.UnregisterAll(this);

Warning: 前述したように、WeakReferenceMessenger 型を使用する場合、受信者を追跡するために弱参照を使用するので、厳密には(登録解除の)必要はありません。(とはいえ)パフォーマンスを向上させるために、受信者の登録を解除することは良い習慣です。
一方で、StrongReferenceMessenger 型の実装では、登録された受信者を追跡するために強参照を使用しています。これはパフォーマンス上の理由から、メモリーリークを避けるために、登録された受信者はそれぞれマニュアルで登録解除する必要があります。受信者が登録されている限り、使用中の StrongReferenceMessenger インスタンスはアクティブな参照を保持し続けて、GCインスタンスを回収できないようにしています。
この処理の設定はマニュアルで行うか ObservableRecipient を継承して行うことができます。ObservableRecipient はデフォルトで受信者が非アクティブになったときには、自動的にすべてのメッセージ登録を削除します。(これについては ObservableRecipient の「ドキュメント」を参照してください)

IRecipient<TMessage> インターフェースを使ってメッセージハンドラを登録することもできます。この場合、各受信者は与えられたメッセージの型に対応するインターフェースを実装すること、メッセージを受信するときに呼び出される Receive(TMessage) メソッドを実装する必要があります。

// Create a message
public class MyRecipient : IRecipient<LoggedInUserChangedMessage>
{
    public void Receive(LoggedInUserChangedMessage message)
    {
        // ここでメッセージを処理
    }
}

// 特定メッセージの登録の例
WeakReferenceMessenger.Default.Register<LoggedInUserChangedMessage>(this);

// 宣言されたハンドラをすべて登録する例
WeakReferenceMessenger.Default.RegisterAll(this);

// 他のモジュールからメッセージを送信する例
WeakReferenceMessenger.Default.Send(new LoggedInUserChangedMessage(user));

Sample

public UserSenderViewModel SenderViewModel { get; } = new UserSenderViewModel();

public UserReceiverViewModel ReceiverViewModel { get; } = new UserReceiverViewModel();

// Simple viewmodel for a module sending a username message
public class UserSenderViewModel : ObservableRecipient
{
    private string username = "Bob";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    public void SendUserMessage()
    {
        Username = Username == "Bob" ? "Alice" : "Bob";

        Messenger.Send(new UsernameChangedMessage(Username));
    }
}

// Simple viewmodel for a module receiving a username message
public class UserReceiverViewModel : ObservableRecipient
{
    private string username = "";

    public string Username
    {
        get => username;
        private set => SetProperty(ref username, value);
    }

    protected override void OnActivated()
    {
        Messenger.Register<UserReceiverViewModel, UsernameChangedMessage>(this, (r, m) => r.Username = m.Value);
    }
}

// A sample message with a username value
public sealed class UsernameChangedMessage : ValueChangedMessage<string>
{
    public UsernameChangedMessage(string value) : base(value)
    {
    }
}
<StackPanel Spacing="8">

    <!--Sender module-->
    <Border BorderBrush="{ThemeResource SystemChromeBlackLowColor}" BorderThickness="1" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.SenderViewModel.Username, Mode=OneWay}"/>
            <Button
                Content="Click to send a message!"
                Click="{x:Bind ViewModel.SenderViewModel.SendUserMessage}"/>
        </StackPanel>
    </Border>

    <!--Receiver module-->
    <Border BorderBrush="{ThemeResource SystemChromeBlackLowColor}" BorderThickness="1" CornerRadius="4" Padding="8">
        <StackPanel Spacing="8">
            <TextBlock Text="{x:Bind ViewModel.ReceiverViewModel.Username, Mode=OneWay}"/>
        </StackPanel>
    </Border>
</StackPanel>

リクエストメッセージの使用 (Request messages - Using request messages)

Messenger インスタンスのもうひとつの便利な機能は、あるモジュールから別のモジュールに値をリクエストするのにも使えることです。そのため、パッケージには RequestMessage<T> クラスが含まれています:

// Create a message
public class LoggedInUserRequestMessage : RequestMessage<User>
{
}

// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    // "CurrentUser" は viewmodel の private メンバーであると仮定します。
    // デリゲート内で "this" をキャプチャするのを避けること。(パフォーマンスが向上)
    m.Reply(r.CurrentUser);
});

// Request the value from another module
User user = WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

RequestMessage<T> クラスは LoggedInUserRequestMessage が含まれている User オブジェクトへの変換を可能にする暗黙のコンバーターを含んでいます。また、このコンバーターはメッセージに対するレスポンスが受信されているかどうかをチェックして、レスポンスが受信されていない場合は例外を throw します。

レスポンスの保証なしでリクエスト・メッセージを送信することも可能です。返却されたメッセージをローカル変数に格納して、レスポンスの値が利用可能かどうかをマニュアルで確認するだけです。そうすることで Send メソッドが返却されたときにレスポンスを受信していなくても、自動的に例外は発生しません。

(上述クラスの定義のある)同じ名前空間には、他のケース用の基本的なリクエスト・メッセージもあります。

  • AsyncRequestMessage
  • CollectionRequestMessage
  • AsyncCollectionRequestMessage

ここでは、非同期リクエスト・メッセージの使い方を説明します:

// Create a message
public class LoggedInUserRequestMessage : AsyncRequestMessage<User>
{
}

// Register the receiver in a module
WeakReferenceMessenger.Default.Register<MyViewModel, LoggedInUserRequestMessage>(this, (r, m) =>
{
    // Task<User> を返信
    m.Reply(r.GetCurrentUserAsync());
});

// 別のモジュールから値をリクエストする
// リクエストに対して await を直接することができる
User user = await WeakReferenceMessenger.Default.Send<LoggedInUserRequestMessage>();

Sample

// ユーザー名のメッセージのリクエストの応答モジュールのためのシンプルな viewmodel
// viewmodel が使用されているときは IsActive を true にすること!
public class UserSenderViewModel : ObservableRecipient
{
    public string Username { get; private set; } = "Bob";

    protected override void OnActivated()
    {
        Messenger.Register<UserSenderViewModel, CurrentUsernameRequestMessage>(this, (r, m) => m.Reply(r.Username));
    }
}

private string username;

public string Username
{
    get => username;
    private set => SetProperty(ref username, value);
}

// 現在のユーザー名を要求するメッセージを送信して、プロパティを更新
public void RequestCurrentUsername()
{
    Username = WeakReferenceMessenger.Default.Send<CurrentUsernameRequestMessage>();
}

// Resets the current username
public void ResetCurrentUsername()
{
    Username = null;
}

// 現在のユーザー名を取得するためのリクエストメッセージ
public sealed class CurrentUsernameRequestMessage : RequestMessage<string>
{
}
<StackPanel Spacing="8">
    <TextBlock Text="{x:Bind ViewModel.Username, Mode=OneWay}"/>
    <Button
        Content="Click to request the username!"
        Click="{x:Bind ViewModel.RequestCurrentUsername}"/>
    <Button
        Content="Click to reset the local username!"
        Click="{x:Bind ViewModel.ResetCurrentUsername}"/>
</StackPanel>

参考