mvvm-toolkit-messenger — скилл для GitHub Copilot: pub/sub в MVVM
Скилл для GitHub Copilot по Messenger из CommunityToolkit.Mvvm: связывает ViewModels через pub/sub, без прямых ссылок друг на друга.
npx skills add github/awesome-copilot --skill mvvm-toolkit-messengerЧто делает mvvm-toolkit-messenger
Скилл разбирает, как устроен Messenger в CommunityToolkit.Mvvm 8.x: какой класс брать по умолчанию, как объявлять сообщения, как подписываться и отписываться. Внутри — готовые примеры кода на C# и таблица выбора между WeakReferenceMessenger.Default и StrongReferenceMessenger.Default.
Отдельно разобраны каналы (tokens) для привязки сообщений к окну или подсистеме, семейство RequestMessage для запросов с ответом и жизненный цикл ObservableRecipient с переключением IsActive. Есть список типичных ошибок: захват this в лямбде, забытый Unregister у strong-ref получателей.
Скилл рассчитан на WPF, WinUI 3, .NET MAUI, Uno и Avalonia. Ставьте, если у вас несколько ViewModels должны реагировать на одно событие и вы не хотите плодить ссылки между ними. Если мессенджер в проекте не используется, проходите мимо.
Какие задачи решает
- связать две ViewModels без прямой ссылки друг на друга
- выбрать между weak- и strong-реализацией мессенджера
- ограничить сообщения одним окном через токен
- запросить значение у другой ViewModels и дождаться ответа
- починить обработчик, который не срабатывает из-за weak-ссылки
Примеры запросов агенту
- Добавь подписку на ThemeChangedMessage через WeakReferenceMessenger.Default
- Сделай запрос CurrentUserRequest с ответом от UserService
- Почему мой обработчик не вызывается при WeakReferenceMessenger?
Как установить mvvm-toolkit-messenger
1Через Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger2GitHub CLI
gh skill install github/awesome-copilot mvvm-toolkit-messenger3Вручную: положить файлы
.github/skills/mvvm-toolkit-messenger/SKILL.md1Через Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger -a claude-code2Вручную: положить файлы
~/.claude/skills/mvvm-toolkit-messenger/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/mvvm-toolkit-messenger/
Вручную: положить файлы
.agents/skills/mvvm-toolkit-messenger/SKILL.mdДля всех проектов — ~/.agents/skills/
Вручную: положить файлы
.cursor/skills/mvvm-toolkit-messenger/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
Командой платформы
gemini skills install https://github.com/github/awesome-copilot --path skills/mvvm-toolkit-messengerВручную: положить файлы
.devin/skills/mvvm-toolkit-messenger/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Claude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
Из того же набора554
Весь набор →Похожие
Вопросы о mvvm-toolkit-messenger
Как установить mvvm-toolkit-messenger в GitHub Copilot?
Через npx skills add github/awesome-copilot --skill mvvm-toolkit-messenger или gh skill install github/awesome-copilot mvvm-toolkit-messenger. Файл скилла окажется в .github/skills/mvvm-toolkit-messenger/SKILL.md.
Что делает mvvm-toolkit-messenger?
Описывает pub/sub из CommunityToolkit.Mvvm: объявление сообщений, подписку, каналы, request/reply и отписку. Помогает агенту писать код мессенджера без типичных ошибок с временем жизни получателей.
С какими платформами работает скилл?
Примеры рассчитаны на WPF, WinUI 3, .NET MAUI, Uno и Avalonia. Нужен проект на CommunityToolkit.Mvvm 8.x.
Исходный текст
SKILL.md · MIT
---
name: mvvm-toolkit-messenger
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.'
---
# 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.
```csharp
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)
```csharp
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
```csharp
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:
```csharp
WeakReferenceMessenger.Default.RegisterAll(this);
```
---
## Send a message
```csharp
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`):
```csharp
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
```csharp
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:
```csharp
var request = WeakReferenceMessenger.Default.Send<CurrentUserRequest>();
if (request.HasReceivedResponse)
User user = request.Response;
```
### Async request
```csharp
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:
```csharp
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:
```csharp
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:
```csharp
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)
```csharp
services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default); // app-wide
services.AddScoped<WindowScopedMessenger>(); // per-window
```
Inject the appropriate `IMessenger` into the ViewModel constructor:
```csharp
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`](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>
© github · MIT · github/awesome-copilot