Skip to content
cccskills
FreePublished on GitHub

mvvm-toolkit-messenger

Connect ViewModels, objects that manage screen state and actions, through messages without direct references. The skill covers event broadcasts, requests for values, window-specific channels, and recipient registration and cleanup using CommunityToolkit.Mvvm.

OriginalShow the original description

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.

See how to install

Good for

  • Share login or theme changes across ViewModels
  • Request values from another ViewModel
  • Keep messages within a specific window
  • Diagnose missing messages and recipient lifetime issues

Files included (2)

  • SKILL.md8.7 KB
  • references/messenger-patterns.md6.9 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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

TypeWhen
WeakReferenceMessenger.DefaultDefault. Recipients held weakly — eligible for GC even while registered. Internal trimming runs during full GCs; no manual Cleanup() needed.
StrongReferenceMessenger.DefaultProfiler shows the messenger is hot and allocation matters. Recipients are pinned until you Unregister. Forgetting unregistration leaks them.
Custom IMessenger instancePer-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

Lambda style (recommended)

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

  1. 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.
  2. 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).
  3. Inherited message types. A handler registered for BaseMessage is not invoked for DerivedMessage : BaseMessage. Register each concrete type.
  4. 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)).
  5. OnActivated never runs. ObservableRecipient only registers IRecipient<T> handlers when IsActive flips from false to true.
  6. 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

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

External:

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Map an existing codebase into seven documents covering its technology, structure, architecture, conventions, integrations, tests, and concerns. Link findings to file evidence, mark unknowns, and highlight differences between stated plans and the actual implementation.

  • Get oriented in an unfamiliar codebase
  • Prepare documentation for a project handover
  • Compare documented architecture with implementation
github/awesome-copilot40.2KUpdated Oct 17, 2026

Assess a repository’s readiness for AI agents with AgentRC and generate a standalone HTML dashboard. Review scores across nine areas, see prioritized improvements, and optionally apply scoring policies tailored to an organization.

  • Assessing a repository’s readiness for AI agents
  • Prioritizing improvements from assessment results
  • Reviewing scores under an organization’s own policy
github/awesome-copilot40.2KUpdated Oct 17, 2026

Generate coding agent instruction files from a project's actual code using AgentRC. Capture detected languages, frameworks, and conventions in one shared file or split rules by topic and project area, with a preview before writing.

  • Create project instructions for VS Code Copilot
  • Refresh agent rules after project changes
  • Split monorepo instructions by project area
github/awesome-copilot40.2KUpdated Oct 17, 2026

Customize AgentRC readiness assessments to match team or organization standards. Choose or create policies that adjust check importance, disable irrelevant checks, set pass thresholds, and define requirements for automated checks in CI workflows.

  • Set shared readiness standards across an organization
  • Focus assessments on AI tooling
  • Adjust the importance of individual checks
github/awesome-copilot40.2KUpdated Oct 17, 2026

Analyze supplied ad performance data to identify wasted spend, strong campaigns, and next tests. Compare channels, assess whether test differences are meaningful, and produce budget reallocation recommendations with a prioritized action plan.

  • Identify ads and keywords to pause
  • Find campaigns and audiences worth scaling
  • Assess whether A/B test results are meaningful
github/awesome-copilot40.2KUpdated Oct 17, 2026

Turn code files into learning resources by adding comments that explain syntax, programming concepts, and design choices. Adjust explanation depth and repetition to the reader’s knowledge while preserving file structure, encoding, and syntax.

  • Add beginner-friendly explanations to existing code
  • Document the reasoning behind syntax and design choices
  • Adjust comment depth for readers with different experience
github/awesome-copilot40.2KUpdated Oct 17, 2026

See all skills by github

Report a problem with this skill