mcpbeat Sign in

Mvvm Toolkit Messenger Agent Skill

CommunityToolkit.Mvvm Messenger pub/sub for decoupled communication between ViewModels (or any objects). Covers WeakReferenceMessenger vs StrongReferenceMessenger, IRecipient<TMessage>, RequestMessage<T> / AsyncRequestMessage<T> / CollectionRequestMessage<T>, ValueChangedMessage<T>, channels (tokens), and the ObservableRecipient activation lifecycle. Use across WPF, WinUI 3, .NET MAUI, Uno, and Avalonia.

4k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
37394
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/github/awesome-copilot --skill mvvm-toolkit-messenger

What comes with it

7 033 bytes besides the instruction
references/messenger-patterns.md

The instruction itself

17 sections, as written by the author

CommunityToolkit.Mvvm Messenger

Pub/sub messaging for ViewModels (or any objects) without forcing a shared

reference graph. Part of CommunityToolkit.Mvvm 8.x.

> TL;DR. Default to WeakReferenceMessenger.Default. Register handlers

> with the (recipient, message) lambda and the static modifier so you

> never capture this. Inherit from ObservableRecipient and toggle

> IsActive at activation/deactivation to get automatic register/unregister.


When to use this skill

  • Two or more ViewModels need to react to an event (login, theme change,

save, navigation) without holding references to each other

  • A ViewModel needs to ask another VM for a value (request/reply)
  • You're scoping events to a sub-system or window with channel tokens
  • Diagnosing "my handler never fires" or weak-reference recipient lifetime

problems

For source generators, base classes, and commands see the mvvm-toolkit

skill. For DI wiring (registering an IMessenger instance), see

mvvm-toolkit-di.


Choose an implementation

| Type | When |

|------|------|

| WeakReferenceMessenger.Default | Default. Recipients held weakly — eligible for GC even while registered. Internal trimming runs during full GCs; no manual Cleanup() needed. |

| StrongReferenceMessenger.Default | Profiler shows the messenger is hot and allocation matters. Recipients are pinned until you Unregister. Forgetting unregistration leaks them. |

| Custom IMessenger instance | Per-window/per-scope (e.g., one messenger per app window). Construct directly, inject via DI. |

ObservableRecipient's parameterless constructor uses

WeakReferenceMessenger.Default. Pass a different IMessenger to its

constructor to override.


Define a message

The toolkit ships base classes; any class works.

using CommunityToolkit.Mvvm.Messaging.Messages;

// Single-payload broadcast
public sealed class LoggedInUserChangedMessage(User user)
    : ValueChangedMessage<User>(user);

// Custom shape (records are great for this)
public sealed record ThemeChangedMessage(AppTheme NewTheme);

// Empty signal
public sealed record RefreshRequestedMessage;

Register a recipient

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

The static modifier prevents accidental closure allocation and keeps

this out of the lambda — use the recipient parameter instead.

IRecipient<TMessage> interface style

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

ObservableRecipient.OnActivated() calls Messenger.RegisterAll(this),

which subscribes every IRecipient<T> interface implemented by the type.

If you're not using ObservableRecipient, register manually:

WeakReferenceMessenger.Default.RegisterAll(this);

Send a message

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

// Empty payloads use the parameterless overload:
WeakReferenceMessenger.Default.Send<RefreshRequestedMessage>();

Channels (tokens)

Scope messages to a sub-system or window with a token (any equatable

value — 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);

Messages sent without a token use the default shared channel — they are

not delivered to channel-scoped recipients.


Request / reply

For ask-style scenarios where a recipient provides a value back to the

sender, use the RequestMessage<T> family.

Sync request

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>();

The implicit conversion from CurrentUserRequest to User throws if no

recipient called Reply. Capture the message to check first:

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

Async request

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>();

Collection requests (fan-in)

CollectionRequestMessage<T> and AsyncCollectionRequestMessage<T> collect

a Reply from every responding recipient:

public sealed class OpenDocumentsRequest : CollectionRequestMessage<Document> { }

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

Lifecycle

Even with WeakReferenceMessenger, unregister explicitly when a recipient

is being torn down — it trims dead entries and improves performance:

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

ObservableRecipient.OnDeactivated() does this automatically when

IsActive flips to false. Set it from your activation hook:

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

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

Common pitfalls

  • Capturing this in the lambda. (r, m) => OnX(m) implicitly

captures this; allocates a closure and confuses lifetime. Always use

(r, m) => r.OnX(m) with static.

  • Strong-ref recipients without Unregister. With

StrongReferenceMessenger, recipients (and their entire object graph)

stay pinned forever. Either inherit from ObservableRecipient

(auto-unregisters in OnDeactivated) or call UnregisterAll(this).

  • Inherited message types. A handler registered for BaseMessage is

not invoked for DerivedMessage : BaseMessage. Register each

concrete type.

  • Wrong messenger instance. Sending via WeakReferenceMessenger.Default

and registering via an injected per-window messenger means the message

never arrives. Use the same IMessenger everywhere (typically inject

it via ObservableRecipient(messenger)).

  • OnActivated never runs. ObservableRecipient only registers

IRecipient<T> handlers when IsActive flips from false to true.

  • Cross-thread updates. The messenger is thread-agnostic. If a

handler updates UI, marshal manually

(DispatcherQueue.TryEnqueue / Dispatcher.BeginInvoke).


Multiple messengers (per-window scoping)

services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>();                       // per-window

Inject the appropriate IMessenger into the ViewModel constructor:

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

This isolates broadcasts to a single window — useful for multi-window

desktop apps (WinUI 3, WPF, MAUI desktop, Avalonia).


References

| Topic | File |

|-------|------|

| Full deep dive (more channel/lifecycle examples, diagnostics) | references/messenger-patterns.md |

External:

  • Messenger docs: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger>
  • WeakReferenceMessenger API: <https://learn.microsoft.com/en-us/dotnet/api/communitytoolkit.mvvm.messaging.weakreferencemessenger>
  • Source: <https://github.com/CommunityToolkit/dotnet>

Other skills for the same job

different authors, same section of the catalogue
Protocolsio Integration
by christophacham
×4

Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.

16k tokens
Tailored Resume Generator
by frostant
×4

Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances

3k tokens
Expo Dev Client
by openai
vendor ×3

Build and distribute Expo development clients locally or via TestFlight

961 tokens
Executing Plans
by ZhanlinCui
×3

Use when you have a written implementation plan to execute in a separate session with review checkpoints

542 tokens
Anndata
by christophacham
×3

Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.

16k tokens
Benchling Integration
by christophacham
×3

Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.

14k tokens
Biopython
by christophacham
×3

Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.

24k tokens
Cellxgene Census
by christophacham
×3

Query the CELLxGENE Census (61M+ cells) programmatically. Use when you need expression data across tissues, diseases, or cell types from the largest curated single-cell atlas. Best for population-scale queries, reference atlas comparisons. For analyzing your own data use scanpy or scvi-tools.

8k tokens

How to use it

Copy the folder

Take github/mvvm-toolkit-messenger from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.