Messaging Between View Models
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Two view models that must react to each other create a design problem. If BooksViewModel holds a reference to
BookEditorViewModel (or the reverse), the two types are welded together, neither can be tested alone, and the
lifetime of one decides the lifetime of the other. Messaging removes the reference: a sender hands a message
object to a shared messenger, and every recipient that registered for that message type receives it. Neither side
knows the other exists; both only agree on the message class. This page covers the messenger in the MVVM Toolkit
(CommunityToolkit.Mvvm), which is the messaging mechanism Microsoft documents for MVVM on .NET. It was written
against .NET 10 LTS (latest patch 10.0.13) with C# 14, Visual Studio 2026 18.11.1, Windows App SDK
2.5.1 and CommunityToolkit.Mvvm 8.4.2 on Windows 10 and Windows 11. .NET 11 is at release candidate 2
(11.0.0-rc.2) and general availability is expected at .NET Conf, 10 to 13 November 2026; the documentation for the
messenger does not list any change for it. The messenger has no dependency on a UI framework, so everything here is the
same on WPF, WinUI 3 and Windows Forms, apart from the threading notes.
The running scenario is Bookshelf Desktop. When the user saves a book in the editor, the list in BooksViewModel
must refresh, and no view model may reference the other. The page builds that, then adds request messages (asking
another view model for a value), channels (tokens) and the lifetime rules that decide whether messaging leaks memory.
For the pattern itself see The MVVM Pattern, and for the base classes and source generators see The MVVM Toolkit;
this page assumes both.
Why a messenger, and when not to use one
The .NET Multi-platform App UI architecture guide explains the motivation. Plain .NET events already implement publish-subscribe, and they are the simplest choice when loose coupling is not needed (a control and the page that contains it). Their cost is that the subscriber type must reference the publisher type, and the lifetimes are tied together: a long-lived or static publisher keeps every subscriber that did not unsubscribe alive, which prevents or delays garbage collection of the subscriber. A messenger breaks both couplings. The guide also lists the alternatives that often fit better than messaging:
-
Data binding and property change notification already carry a view model’s state to its view.
-
Passing data during navigation is the direct way for one view model to hand a value to the next (see Navigation, Dialogs & Services in MVVM).
-
A shared service registered in dependency injection (an
IBookRepository, a store object) is explicit and easy to follow; two view models can both depend on it.
Messaging trades explicitness for decoupling. The sender cannot see who reacts, a message that nobody receives is silently dropped, and a debugger shows no call from sender to recipient. Use it for notifications that are genuinely "something happened, whoever cares may react" (a book was saved, the user logged out, the theme changed), and use a service or navigation parameter when exactly one known collaborator needs the data.
The messenger types
The contract is IMessenger in the CommunityToolkit.Mvvm.Messaging namespace. The package supplies two
implementations, and both expose a thread-safe Default instance (WeakReferenceMessenger.Default,
StrongReferenceMessenger.Default).
| Aspect | WeakReferenceMessenger |
StrongReferenceMessenger |
|---|---|---|
References to recipients |
Weak. An unused recipient stays eligible for garbage collection even if it still has registered handlers. |
Strong. While a recipient is registered the messenger keeps it alive; each recipient must be unregistered manually. |
Performance and memory |
Slower and uses more memory than the strong one. |
Better performance and far less memory usage. |
Cleanup needed |
Not required for correctness; still good practice, to improve performance. |
Required, otherwise the recipient leaks. |
Typical fit |
Subscribers without a clearly defined lifecycle (the guide’s wording). |
A workflow with a tightly controlled lifetime, such as a subscription tied to a page appearing and disappearing. |
Default of |
Yes ( |
Only if you pass an instance to the constructor. |
Source: Messenger
Source: Communicating Between Components
The documentation states that the weak messenger is simpler to use and matches the behaviour of the messenger in the older MvvmLight library, which is why it is the default. You can also create more than one messenger instance, for example one per module or per window group, and inject it through the service provider.
Members of IMessenger
| Member | Purpose |
|---|---|
|
The single primitive registration. All the convenience overloads call it. |
|
Delivers a message to every recipient registered for that message type on that channel. |
|
Removes one message type on one channel for a recipient. |
|
Removes a recipient from all messages on all channels. |
|
Removes a recipient from all messages on one channel. |
|
Reports whether the recipient is registered for the message. |
|
Trims the internal data structures. It does not unregister anything. |
|
Unregisters all recipients. Useful between unit tests that share a messenger. |
The everyday overloads are extension methods in IMessengerExtensions: Register<TMessage>(recipient) for an
IRecipient<TMessage>, RegisterAll(recipient) and RegisterAll(recipient, token), Register<TRecipient,TMessage>(recipient,
handler), Send<TMessage>(message), Send<TMessage>() and Unregister<TMessage>(recipient). There is also
CreateObservable<TMessage>(), which exposes a message type as an IObservable<T> for Rx-style code.
Source: IMessenger interface
Defining a message
A message is an ordinary class, defined by what happened and what data goes with it. The toolkit supplies base classes
for the common shapes; ValueChangedMessage<T> carries a single value in its Value property. For Bookshelf Desktop
the "book saved" fact is a message holding the saved Book:
using CommunityToolkit.Mvvm.Messaging.Messages;
using Bookshelf.Core;
namespace Bookshelf.Desktop.Messages;
/// <summary>Sent after a book was added or updated in the repository.</summary>
public sealed class BookSavedMessage(Book book) : ValueChangedMessage<Book>(book);
/// <summary>Sent after a book was deleted from the repository.</summary>
public sealed class BookDeletedMessage(int id) : ValueChangedMessage<int>(id);
Source: Communicating Between Components
The guide’s own example defines AddProductMessage : ValueChangedMessage<int> with a constructor that passes the value
to the base class; the primary-constructor form above is the C# 12 and later spelling of the same idea. Three rules keep
messages healthy:
-
Immutable payloads. The guide warns that several threads can read the payload at once, so the handler must not modify it.
Bookis already an immutable positional record, which is the reason Bookshelf Desktop uses it. -
One fact per class. Subscribers register for a specific type. The default messengers do not deliver inherited message types, so a handler registered for a base class never receives a derived message.
-
Name it as an event in the past tense (
BookSavedMessage) for notifications, and as a question (SelectedBookRequestMessage) for requests.
Sending and receiving: the book-saved flow
The editor saves through the repository and then announces it. The recipient is the list view model.
The figure shows that the editor never touches BooksViewModel. The messenger looks up every recipient registered for
BookSavedMessage and calls its handler on the sending thread, so by the time Send returns every handler has run.
The sender
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Bookshelf.Core;
using Bookshelf.Desktop.Messages;
namespace Bookshelf.Desktop.ViewModels;
// Excerpt: the canonical editor of the MVVM Toolkit page, BookEditorViewModel(Book book), with the repository and
// the messenger added as constructor parameters. Its other properties and validation members are omitted.
public sealed partial class BookEditorViewModel(
Book book, IBookRepository repository, IMessenger messenger) : ObservableValidator
{
[ObservableProperty]
public partial string Title { get; set; } = book.Title;
[ObservableProperty]
public partial string Author { get; set; } = book.Author;
public Book ToBook() => book with { Title = Title, Author = Author };
[RelayCommand]
private async Task SaveAsync(CancellationToken cancellationToken)
{
ValidateAllProperties();
if (HasErrors)
{
return;
}
var saved = ToBook();
if (saved.Id == 0)
{
saved = await repository.AddAsync(saved, cancellationToken);
}
else
{
await repository.UpdateAsync(saved, cancellationToken);
}
messenger.Send(new BookSavedMessage(saved));
}
}
Source: Messenger
The messenger is injected as an IMessenger rather than read from WeakReferenceMessenger.Default, so a unit test can
pass a fresh instance. Send is fire-and-forget: it also succeeds when nobody is registered, in which case the message
is ignored.
The recipient as an ObservableRecipient
ObservableRecipient extends ObservableObject and is the base class meant for view models that use the messenger.
Implementing IRecipient<TMessage> for each message type is the declarative way to declare handlers.
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Bookshelf.Core;
using Bookshelf.Desktop.Messages;
namespace Bookshelf.Desktop.ViewModels;
// Messenger-aware variant of BooksViewModel: the base class becomes ObservableRecipient
// and the messenger is injected as a third constructor argument.
public sealed partial class BooksViewModel(
IBookRepository repository, IDialogService dialogs, IMessenger messenger)
: ObservableRecipient(messenger),
IRecipient<BookSavedMessage>,
IRecipient<BookDeletedMessage>
{
public ObservableCollection<BookViewModel> Books { get; } = [];
[ObservableProperty]
public partial BookViewModel? SelectedBook { get; set; }
[ObservableProperty]
public partial string StatusMessage { get; set; } = string.Empty;
// Called for every BookSavedMessage while this view model is active.
public void Receive(BookSavedMessage message)
{
var saved = message.Value;
var existing = Books.FirstOrDefault(b => b.Id == saved.Id);
if (existing is null)
{
Books.Add(new BookViewModel(saved));
StatusMessage = $"Added {saved.Title}";
return;
}
Books[Books.IndexOf(existing)] = new BookViewModel(saved);
StatusMessage = $"Updated {saved.Title}";
}
public void Receive(BookDeletedMessage message)
{
var existing = Books.FirstOrDefault(b => b.Id == message.Value);
if (existing is not null)
{
Books.Remove(existing);
}
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task LoadAsync(CancellationToken cancellationToken)
{
Books.Clear();
foreach (var book in await repository.GetAllAsync(cancellationToken))
{
Books.Add(new BookViewModel(book));
}
}
}
Source: ObservableRecipient
Replacing the element at the same index preserves the position; re-selecting is the view’s concern. Because
BookViewModel wraps an immutable Book, replacing the wrapper is the correct way to show a new value. The sample
assumes IsFavorite and other view-only state are re-read from the repository or carried over in a real application.
Activation: when does the registration happen
Constructing the view model does not register it. ObservableRecipient exposes IsActive; setting it to true
calls OnActivated(), which by default registers every IRecipient<TMessage> the class implements, and setting it to
false calls OnDeactivated(), which by default unregisters the instance from all messages. The documentation
recommends registering in OnActivated and cleaning up in OnDeactivated, because this lets the same view model be
enabled and disabled any number of times and still be safe to collect.
using System.Windows;
namespace Bookshelf.Desktop.Views;
public partial class BooksView : System.Windows.Controls.UserControl
{
public BooksView(ViewModels.BooksViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
Loaded += (_, _) => viewModel.IsActive = true;
Unloaded += (_, _) => viewModel.IsActive = false;
}
}
Source: ObservableRecipient
The activation call is the view’s job (or the navigation service’s, see Navigation, Dialogs & Services in MVVM), so
that the view model stays free of UI types. The documentation does not prescribe which UI event should drive it. In
WPF, Loaded can fire more than once for an element that is removed and added back to the tree, for example when a tab
is switched, and Unloaded does not run in every shutdown path; setting IsActive to its current value changes
nothing, so the pairing above is safe to repeat.
Registering by hand
IRecipient<T> is not mandatory. A view model can register a lambda or a method group in OnActivated. Taking the
recipient as the first lambda parameter, and not touching this, is the documented way to avoid capturing the instance
and to improve performance.
protected override void OnActivated()
{
// Default channel, lambda that does not capture "this".
Messenger.Register<BooksViewModel, BookSavedMessage>(this, (recipient, message) =>
{
recipient.StatusMessage = $"Saved {message.Value.Title}";
});
// Method group: a static method with the (recipient, message) signature.
Messenger.Register<BooksViewModel, BookDeletedMessage>(this, OnBookDeleted);
}
private static void OnBookDeleted(BooksViewModel recipient, BookDeletedMessage message)
{
var existing = recipient.Books.FirstOrDefault(b => b.Id == message.Value);
if (existing is not null)
{
recipient.Books.Remove(existing);
}
}
Source: Messenger
If you override OnActivated and still want the IRecipient<T> handlers registered, call base.OnActivated(). That
base call is what performs the automatic registration (documented behaviour: "by default OnActivated will
automatically register all the message handlers defined through the IRecipient<TMessage> interface").
Request messages
Messages can also ask. A request message carries a reply slot; a recipient fills it with Reply, and the sender reads
the answer from the value returned by Send. This is request-response between modules without a reference, with the
same caveat as any messaging: there must be a recipient.
| Base class | Use |
|---|---|
|
One synchronous reply of type |
|
One reply that is a |
|
Any number of replies, enumerated by the sender. |
|
Any number of replies, some of them tasks, enumerated asynchronously. |
Synchronous request
using CommunityToolkit.Mvvm.Messaging.Messages;
using Bookshelf.Core;
namespace Bookshelf.Desktop.Messages;
/// <summary>Asks whichever view model owns the list for the currently selected book.</summary>
public sealed class SelectedBookRequestMessage : RequestMessage<Book?>;
Source: Messenger
// In BooksViewModel.OnActivated (or any active recipient):
Messenger.Register<BooksViewModel, SelectedBookRequestMessage>(this, (recipient, message) =>
{
message.Reply(recipient.SelectedBook?.Model);
});
// In a status bar view model that holds an IMessenger:
Book? selected = messenger.Send<SelectedBookRequestMessage>();
Source: Messenger
RequestMessage<T> has an implicit conversion to T. The conversion throws if no recipient replied, so a request
with no active recipient fails loudly instead of returning a default. To avoid the exception, store the returned
message in a local variable and test whether a response is available before reading it. The page for the messenger does
not name the exception type or the members used for that check; read the API reference for RequestMessage<T>.
Asynchronous request
An async request lets the recipient do I/O before it answers. Here the editor asks the shell whether it may close without losing work; the shell shows a confirmation dialog.
public sealed class ConfirmCloseRequestMessage : AsyncRequestMessage<bool>;
// Recipient: a ShellViewModel whose ConfirmDiscardAsync method shows the dialog.
Messenger.Register<ShellViewModel, ConfirmCloseRequestMessage>(this, (recipient, message) =>
{
message.Reply(recipient.ConfirmDiscardAsync()); // returns Task<bool>
});
// Sender: the request message is awaitable.
bool mayClose = await messenger.Send<ConfirmCloseRequestMessage>();
Source: Messenger
The documented example replies with a Task<User> and the sender awaits the message directly. For a result that one
view model computes for another over many steps, prefer a service: a request message hides the call target and
cannot express cancellation.
Tokens and channels
Messages sent with no token travel on the default shared channel. A token identifies a private channel, so two
modules can exchange messages of the same type without receiving each other’s. The token type parameter is
constrained to IEquatable<TToken>, so a string, an int, a Guid or a readonly record struct works; an enum
does not satisfy the constraint and fails to compile. Registrations and sends that use different tokens never meet.
/// <summary>String tokens that name the messaging channels of each shelf.</summary>
public static class ShelfChannels
{
public const string Library = "library";
public const string Wishlist = "wishlist";
}
// Receive only the wishlist shelf's saves.
Messenger.Register<WishlistViewModel, BookSavedMessage, string>(
this, ShelfChannels.Wishlist, (recipient, message) => recipient.Add(message.Value));
// Send to that channel only.
messenger.Send(new BookSavedMessage(book), ShelfChannels.Wishlist);
// Unregister the message type on that channel, or everything on that channel.
messenger.Unregister<BookSavedMessage, string>(this, ShelfChannels.Wishlist);
messenger.UnregisterAll(this, ShelfChannels.Wishlist);
// Register every IRecipient<T> the class implements on a channel.
messenger.RegisterAll(this, ShelfChannels.Wishlist);
Source: Messenger
A message sent on a token is not delivered to recipients registered on the default channel, and the reverse. The
Unregister calls above follow the same rule: UnregisterAll(recipient) clears every channel, and the token overload
only one. Another way to separate modules is to use several messenger instances: for example one per window, injected
through the service provider (see Dependency Injection & the Generic Host). Channels are the lighter tool; separate
instances give stronger isolation and make Reset() cheap in tests.
Broadcasting property changes
ObservableRecipient can announce that one of its properties changed, using PropertyChangedMessage<T>. Its
Broadcast<T>(oldValue, newValue, propertyName) method sends the message through the instance’s Messenger. The
SetProperty overloads that take a bool broadcast argument call it for you, so a view model can opt in per property
(SetProperty(ref field, value, true)). Other view models register for PropertyChangedMessage<T> with the concrete
T they care about, and read the sender, the property name and the old and new values from the message.
Source: ObservableRecipient
This sits beside, not in place of, normal binding: the view still binds to the property directly. Use broadcast only
when another view model needs the change. For generated properties, the [NotifyPropertyChangedRecipients] attribute
does the same: on a type that inherits from ObservableRecipient, the generated setter calls
Broadcast(oldValue, value) after a successful change (see the MVVM Toolkit (CommunityToolkit.Mvvm)
page).
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial Book? SelectedBook { get; set; }
Source: ObservableProperty attribute
Threading
The messenger does no marshalling. A message is delivered synchronously, on the thread that called Send. The MAUI
architecture guide says this explicitly: messages are published from the thread the sender is running on and
subscribers receive them on the same thread, and a subscriber that updates the UI from a message sent on a background
thread must marshal to the UI thread itself.
| Stack | How a recipient marshals to the UI thread | Notes |
|---|---|---|
WPF |
|
|
WinUI 3 |
|
The queue belongs to the window’s thread; get it from a control or |
Windows Forms |
|
Not applicable when |
.NET MAUI (for comparison) |
|
The guide marks |
The simplest rule is to send from the UI thread, which is what the editor above does: after await in a command, WPF
and WinUI 3 continue on the UI thread, so Send runs there. If a service sends from a background thread, wrap the
update in the recipient. The details of the dispatcher types are in Threading, Dispatcher & Async.
Source: Communicating Between Components
Leaks and lifetime
Whether messaging leaks memory is decided by who holds the reference to the recipient.
With the weak messenger a forgotten recipient is eventually collected. Between becoming unreachable and being
collected it is still registered, so it can keep reacting to messages, which is a correctness problem (a closed editor
that still writes to a status bar) even though it is not a leak. With the strong messenger the messenger keeps every
registered recipient alive for as long as it exists. Since Default is a static instance, a view model that never
unregisters lives for the whole process.
The practical rules:
-
Prefer the activation pattern. Register in
OnActivated, letOnDeactivatedunregister, and setIsActivefrom the view’sLoadedandUnloaded(or the navigation service). This is the documented recommendation, and it keeps both messengers correct. -
Use the strong messenger only with a controlled lifetime (a page that appears and disappears), and then always deactivate. If you pass
StrongReferenceMessenger.DefaulttoObservableRecipient, the base class still unregisters for you on deactivation. -
Do not hand-roll the same thing with a plain
+=event on a long-lived object. That is the situation the guide describes: the subscriber is kept alive by the publisher’s reference. -
Call
Cleanup()rarely. It only trims the messenger’s internal structures; it does not unregister anyone. -
Do not register a recipient twice for the same message and channel. The toolkit documents
IsRegisteredfor checking, and registering the same recipient twice for one message and channel is not a supported way to replace a handler. The documentation for the messenger does not state what happens, so check withIsRegisteredinstead of relying on it.
Source: Messenger
The messenger in each stack
| Topic | WPF | WinUI 3 | Windows Forms |
|---|---|---|---|
Package |
|
|
|
Where the activation call goes |
|
|
Form |
UI-thread marshalling |
|
|
|
Message and view-model code |
Identical |
Identical |
Identical (if you use a presenter instead of binding) |
The same BookSavedMessage therefore works unchanged in a WPF main window, a WinUI 3 page and a Windows Forms form
that uses a presenter, which is a practical benefit of keeping messages in a UI-free assembly such as Bookshelf.Core
or a Messages folder of the view-model project. The WinUI 3 guidance on data binding and MVVM names the toolkit as
Microsoft’s recommended MVVM library; it does not add messenger-specific guidance.
What changed over time
| Version | Messaging change |
|---|---|
.NET Framework with MvvmLight |
|
.NET MAUI (before the toolkit messenger) |
|
MVVM Toolkit 7 (as |
|
MVVM Toolkit 8.x |
Namespaces are |
.NET 8, 9, 10, 11 |
No messaging change is documented. The toolkit is a separate NuGet package, not a part of the framework. |
Pitfalls
-
Forgetting to activate. An
ObservableRecipientreceives nothing untilIsActiveistrue; a view model that is constructed but never activated looks like "the message was not sent". -
Registering in the constructor with the strong messenger. The registration outlives the view model unless something unregisters it. Register in
OnActivated. -
Inherited message types. Registering for a base type does not receive derived messages in the default messengers.
-
Mutable payloads. Several recipients read the same object; mutate a copy, or use immutable records.
-
Sending from a background thread to a UI-bound recipient. The handler runs on the sender’s thread. Marshal in the recipient, or send from the UI thread.
-
A request with no recipient. Converting a
RequestMessage<T>toTthrows when nobody replied. Check first, or guarantee a recipient is active. -
Hidden dependencies. A view model whose behaviour depends on a message from nowhere is harder to understand. Keep message classes in one folder, name them as events or requests, and document who sends and who receives.
-
Using
Defaultin tests. Tests that shareWeakReferenceMessenger.Defaultcan see each other’s recipients. Createnew WeakReferenceMessenger()per test and inject it, or callReset(). -
Treating messaging as a mediator. The messenger does not order handlers, retry, persist or return results from many recipients except through
Collectionrequest messages. If you need those, use a service or a dedicated library; third-party mediator and event-aggregator libraries exist but are outside Microsoft documentation. -
Deadlocks by blocking on an async request. Awaiting
Sendis the supported form; calling.Resulton the returned message from the UI thread blocks the dispatcher.
What the documentation does not provide
-
The messenger and
ObservableRecipientpages are the toolkit’s conceptual pages; the Learn text has not been rewritten for the 8.4 release, and its code samples omitusingdirectives. The namespaces used here areCommunityToolkit.Mvvm.MessagingandCommunityToolkit.Mvvm.Messaging.Messages. -
There is no documented guidance on message ordering, handler exceptions or re-entrancy (a handler that sends another message). Test your own application.
-
There is no documented benchmark for the weak versus strong messengers beyond the qualitative statement of better performance and lower memory for the strong one. Measure before choosing it for performance reasons.
-
Which UI event should set
IsActiveis not specified; theLoaded/Unloadedpairing, the navigation service hook and the message classes in this page are patterns of this guide, not official samples. The Bookshelf types are original. -
AsyncRequestMessage<T>details (a second reply, what happens with no recipient) are not described on the messenger page; the exception behaviour stated forRequestMessage<T>is the only one documented there. -
No documentation covers messaging in WPF or WinUI 3 specifically. The MAUI chapter is the only Microsoft guide with a worked messenger example, and it targets .NET MAUI.
References
-
Communicating Between Components (Enterprise Application Patterns Using .NET MAUI)
-
Threading, Dispatcher & Async for marshalling to the UI thread
-
Windows, Dialogs & Navigation for window and page lifetime events
-
Commands for the
[RelayCommand]commands used in the examples -
Data Binding for how the list refresh reaches the view
-
Validation for
ObservableValidator, the base of the editor