mvvm-toolkit-messenger

Par github · awesome-copilot

CommunityToolkit.Mvvm Messenger pub/sub pour la communication découplée entre ViewModels (ou tout objet). Couvre WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, les canaux (tokens), et le cycle de vie d'activation ObservableRecipient. Utilisable avec WPF, WinUI 3, .NET MAUI, Uno et Avalonia.

npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkit-messenger

CommunityToolkit.Mvvm Messenger

Messagerie Pub/sub pour ViewModels (ou n'importe quels objets) sans forcer un graphe de références partagées. Fait partie de CommunityToolkit.Mvvm 8.x.

TL;DR. Utilisez par défaut WeakReferenceMessenger.Default. Enregistrez les gestionnaires avec le lambda (recipient, message) et le modificateur static pour ne jamais capturer this. Héritez de ObservableRecipient et basculez IsActive à l'activation/désactivation pour obtenir l'enregistrement/désenregistrement automatique.


Quand utiliser cette skill

  • Deux ou plusieurs ViewModels doivent réagir à un événement (login, changement de thème, sauvegarde, navigation) sans se référencer mutuellement
  • Un ViewModel doit demander une valeur à un autre VM (requête/réponse)
  • Vous délimitez les événements à un sous-système ou une fenêtre avec des tokens de canal
  • Diagnostiquer les problèmes « mon gestionnaire ne s'exécute jamais » ou de durée de vie des destinataires avec références faibles

Pour les générateurs de source, les classes de base et les commandes, consultez la skill mvvm-toolkit. Pour le câblage d'injection de dépendances (enregistrement d'une instance IMessenger), consultez mvvm-toolkit-di.


Choisir une implémentation

Type Quand
WeakReferenceMessenger.Default Par défaut. Les destinataires sont tenus faiblement — éligibles au GC même s'ils sont enregistrés. Le nettoyage interne s'exécute lors des GC complets ; aucun Cleanup() manuel nécessaire.
StrongReferenceMessenger.Default Le profileur montre que le messenger est chaud et l'allocation importe. Les destinataires sont épinglés jusqu'à ce que vous appeliez Unregister. Oublier la désinscription les fuit.
Instance IMessenger personnalisée Par fenêtre/par portée (p. ex., un messenger par fenêtre d'application). Construisez directement, injectez via l'injection de dépendances.

Le constructeur sans paramètres de ObservableRecipient utilise WeakReferenceMessenger.Default. Passez un IMessenger différent à son constructeur pour le remplacer.


Définir un message

La boîte à outils fournit des classes de base ; n'importe quelle classe fonctionne.

using CommunityToolkit.Mvvm.Messaging.Messages;

// Diffusion avec un seul payload
public sealed class LoggedInUserChangedMessage(User user)
    : ValueChangedMessage<User>(user);

// Forme personnalisée (les records sont parfaits pour cela)
public sealed record ThemeChangedMessage(AppTheme NewTheme);

// Signal vide
public sealed record RefreshRequestedMessage;

Enregistrer un destinataire

Style lambda (recommandé)

WeakReferenceMessenger.Default.Register<MyViewModel, ThemeChangedMessage>(
    this,
    static (recipient, message) => recipient.OnThemeChanged(message.NewTheme));

Le modificateur static empêche l'allocation accidentelle de fermeture et garde this en dehors du lambda — utilisez le paramètre recipient à la place.

Style interface IRecipient<TMessage>

public sealed class MyViewModel : ObservableRecipient,
    IRecipient<ThemeChangedMessage>,
    IRecipient<RefreshRequestedMessage>
{
    public void Receive(ThemeChangedMessage message) { /* ... */ }
    public void Receive(RefreshRequestedMessage message) { /* ... */ }
}

ObservableRecipient.OnActivated() appelle Messenger.RegisterAll(this), qui souscrit à chaque interface IRecipient<T> implémentée par le type. Si vous n'utilisez pas ObservableRecipient, enregistrez manuellement :

WeakReferenceMessenger.Default.RegisterAll(this);

Envoyer un message

WeakReferenceMessenger.Default.Send(new ThemeChangedMessage(AppTheme.Dark));

// Les payloads vides utilisent la surcharge sans paramètres :
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();

Canaux (tokens)

Délimitez les messages à un sous-système ou une fenêtre avec un token (n'importe quelle valeur équitable — int, string, Guid) :

const int LeftPaneChannel = 1;

WeakReferenceMessenger.Default.Register<MyViewModel, RefreshRequestedMessage, int>(
    this, LeftPaneChannel,
    static (r, _) => r.RefreshLeft());

WeakReferenceMessenger.Default.Send(new RefreshRequestedMessage(), LeftPaneChannel);

Les messages envoyés sans token utilisent le canal partagé par défaut — ils ne sont pas livrés aux destinataires délimités par canal.


Requête / réponse

Pour les scénarios de type demande où un destinataire fournit une valeur à l'expéditeur, utilisez la famille RequestMessage<T>.

Requête synchrone

public sealed class CurrentUserRequest : RequestMessage<User> { }

WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
    this,
    static (r, m) => m.Reply(r.CurrentUser));

User user = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();

La conversion implicite de CurrentUserRequest en User lève une exception si aucun destinataire n'a appelé Reply. Capturez le message pour vérifier d'abord :

var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
    User user = request.Response;

Requête asynchrone

public sealed class CurrentUserRequest : AsyncRequestMessage<User> { }

WeakReferenceMessenger.Default.Register<UserService, CurrentUserRequest>(
    this,
    static (r, m) => m.Reply(r.GetCurrentUserAsync()));

User user = await WeakReferenceMessenger.Default.Send<CurrentUserRequest>();

Requêtes de collections (fan-in)

CollectionRequestMessage<T> et AsyncCollectionRequestMessage<T> collectent une Reply de chaque destinataire répondant :

public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }

var docs = WeakReferenceMessenger.Default.Send<OpenDocumentsRequest>();
foreach (Document doc in docs) { /* ... */ }

Cycle de vie

Même avec WeakReferenceMessenger, désenregistrez explicitement quand un destinataire est démoli — cela supprime les entrées mortes et améliore les performances :

WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage>(this);
WeakReferenceMessenger.Default.Unregister<ThemeChangedMessage, int>(this, LeftPaneChannel);
WeakReferenceMessenger.Default.UnregisterAll(this);

ObservableRecipient.OnDeactivated() le fait automatiquement quand IsActive bascule à false. Définissez-le à partir de votre hook d'activation :

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    base.OnNavigatedTo(e);
    ViewModel.IsActive = true;
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    ViewModel.IsActive = false;
    base.OnNavigatedFrom(e);
}

Pièges courants

  1. Capturer this dans le lambda. (r, m) => OnX(m) capture implicitement this ; alloue une fermeture et confond la durée de vie. Utilisez toujours (r, m) => r.OnX(m) avec static.
  2. Destinataires avec références fortes sans Unregister. Avec StrongReferenceMessenger, les destinataires (et tout leur graphe d'objets) restent épinglés pour toujours. Héritez soit de ObservableRecipient (désenregistrement automatique dans OnDeactivated) soit appelez UnregisterAll(this).
  3. Types de messages hérités. Un gestionnaire enregistré pour BaseMessage n'est pas invoqué pour DerivedMessage : BaseMessage. Enregistrez chaque type concret.
  4. Mauvaise instance de messenger. Envoyer via WeakReferenceMessenger.Default et enregistrer via un messenger injecté par fenêtre signifie que le message n'arrive jamais. Utilisez le même IMessenger partout (généralement injectez-le via ObservableRecipient(messenger)).
  5. OnActivated ne s'exécute jamais. ObservableRecipient n'enregistre les gestionnaires IRecipient<T> que quand IsActive bascule de false à true.
  6. Mises à jour multi-threads. Le messenger est indépendant des threads. Si un gestionnaire met à jour l'interface utilisateur, organisez manuellement (DispatcherQueue.TryEnqueue / Dispatcher.BeginInvoke).

Messengers multiples (délimitation par fenêtre)

services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // à l'échelle de l'app
services.AddScoped<WindowScopedMessenger>();                       // par fenêtre

Injectez le IMessenger approprié dans le constructeur du ViewModel :

public sealed partial class WindowViewModel(IMessenger messenger)
    : ObservableRecipient(messenger) { }

Cela isole les diffusions à une seule fenêtre — utile pour les applications de bureau multi-fenêtres (WinUI 3, WPF, MAUI desktop, Avalonia).


Références

Sujet Fichier
Plongée approfondie complète (plus d'exemples de canaux/cycle de vie, diagnostics) references/messenger-patterns.md

Externe :

Skills similaires