mvvm-toolkit — GitHub Copilot skill: what it does, how to install
Guides GitHub Copilot to scaffold ViewModels, source generator attributes, commands, and validation for CommunityToolkit.Mvvm.
npx skills add github/awesome-copilot --skill mvvm-toolkitWhat mvvm-toolkit does
This skill guides the assistant when writing C# ViewModels backed by CommunityToolkit.Mvvm. It instructs Copilot to place [ObservableProperty] on private fields and [RelayCommand] on methods. It also configures dependent notifications like [NotifyPropertyChangedFor] and [NotifyCanExecuteChangedFor]. Classes must be partial. Missing the partial modifier triggers generator diagnostic errors.
The skill relies on base classes like ObservableObject and ObservableValidator. It also covers cancellation tokens on asynchronous commands. If your project avoids source generators or uses a different MVVM framework, you do not need this skill. For XAML developers on WPF, MAUI, or Avalonia, it cuts boilerplate code immediately.
What it helps with
- generate observable properties from private fields with source generators
- create asynchronous relay commands with cancellation tokens
- wire property change notifications to dependent properties and commands
- configure model validation rules using ObservableValidator
Example prompts for the agent
- Create a partial ContactViewModel with an observable Name property
- Add a LoadCommand that runs asynchronously with a CancellationToken
- Set up an ObservableValidator with required fields for user registration
How to install mvvm-toolkit
1Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit2GitHub CLI
gh skill install github/awesome-copilot mvvm-toolkit3Manually: put the files in place
.github/skills/mvvm-toolkit/SKILL.md1Skills CLI (Vercel)
npx skills add github/awesome-copilot --skill mvvm-toolkit -a claude-code2Manually: put the files in place
~/.claude/skills/mvvm-toolkit/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/mvvm-toolkit/
Manually: put the files in place
.agents/skills/mvvm-toolkit/SKILL.mdFor all projects use ~/.agents/skills/
Manually: put the files in place
.cursor/skills/mvvm-toolkit/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
Platform CLI
gemini skills install https://github.com/github/awesome-copilot --path skills/mvvm-toolkitManually: put the files in place
.devin/skills/mvvm-toolkit/SKILL.mdThe legacy .windsurf/skills/ path still works
Claude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set554
The whole set →Similar
Questions about mvvm-toolkit
How do I install mvvm-toolkit in GitHub Copilot?
Run gh skill install github/awesome-copilot mvvm-toolkit or add .github/skills/mvvm-toolkit/SKILL.md to your repository.
What does the mvvm-toolkit skill do?
It provides patterns and prompts for CommunityToolkit.Mvvm, ensuring Copilot uses source generators and partial classes correctly.
Why must classes using ObservableProperty be marked partial?
The toolkit source generators produce the property implementations in separate code files. Without the partial keyword, the compiler fails with diagnostic errors.
Source text
SKILL.md · MIT
---
name: mvvm-toolkit
description: 'CommunityToolkit.Mvvm (the MVVM Toolkit) core: source generators ([ObservableProperty], [RelayCommand], [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyDataErrorInfo]), base classes (ObservableObject / ObservableValidator / ObservableRecipient), commands (RelayCommand / AsyncRelayCommand), and validation. Companion skills: mvvm-toolkit-messenger for pub/sub, mvvm-toolkit-di for Microsoft.Extensions.DependencyInjection wiring. Works across WPF, WinUI 3, MAUI, Uno, and Avalonia.'
---
# CommunityToolkit.Mvvm (core)
Use this skill when authoring or reviewing ViewModels, properties,
commands, or validation in apps that use `CommunityToolkit.Mvvm` 8.x.
> **Companion skills.** Load **`mvvm-toolkit-messenger`** for `IMessenger`
> pub/sub patterns. Load **`mvvm-toolkit-di`** for
> `Microsoft.Extensions.DependencyInjection` integration.
> **Quick recap.** `[ObservableProperty]` on private fields in `partial`
> classes; `[RelayCommand]` on instance methods; inherit from
> `ObservableObject` (or `ObservableValidator` for input forms,
> `ObservableRecipient` when using `IMessenger`).
---
## Package & setup
```xml
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>
```
Targets: `netstandard2.0`, `netstandard2.1`, `net6.0`+. Works on .NET, .NET
Framework, Mono. Source generators ship in the same NuGet — no extra
analyzer reference required.
Namespaces:
```csharp
using CommunityToolkit.Mvvm.ComponentModel; // ObservableObject, [ObservableProperty]
using CommunityToolkit.Mvvm.Input; // [RelayCommand], RelayCommand, AsyncRelayCommand
```
> **Universal rule.** Every type that uses `[ObservableProperty]` or
> `[RelayCommand]` — and every enclosing type, if nested — must be
> declared `partial`. Without it, the generators emit
> `MVVMTK0008` / `MVVMTK0042`.
---
## Source generators cheat sheet
| Attribute | Applied to | Generates |
|-----------|-----------|-----------|
| `[ObservableProperty]` | private field | Public `INotifyPropertyChanged` property + `OnXxxChanging`/`OnXxxChanged` partial-method hooks |
| `[NotifyPropertyChangedFor(nameof(Other))]` | observable field | Also raises `PropertyChanged` for the listed property |
| `[NotifyCanExecuteChangedFor(nameof(MyCommand))]` | observable field | Calls `MyCommand.NotifyCanExecuteChanged()` on change |
| `[NotifyDataErrorInfo]` | observable field on `ObservableValidator` | Calls `ValidateProperty(value)` from the setter |
| `[NotifyPropertyChangedRecipients]` | observable field on `ObservableRecipient` | `Broadcast(old, new)` after the change |
| `[RelayCommand]` | instance method | Lazy `RelayCommand` / `AsyncRelayCommand` exposed as `IRelayCommand` / `IAsyncRelayCommand` |
| `[RelayCommand(CanExecute = nameof(CanX))]` | instance method | Wires `CanExecute` to a method or property |
| `[RelayCommand(IncludeCancelCommand = true)]` | async method with `CancellationToken` | Also generates `XxxCancelCommand` |
| `[RelayCommand(AllowConcurrentExecutions = true)]` | async method | Allows queued/parallel invocations (default disables while running) |
| `[RelayCommand(FlowExceptionsToTaskScheduler = true)]` | async method | Surfaces exceptions via `ExecutionTask` instead of awaiting and rethrowing |
| `[property: SomeAttr]` | observable field or `[RelayCommand]` method | Forwards `SomeAttr` onto the generated property (e.g., `[JsonIgnore]`) |
**Naming.** Field `name` / `_name` / `m_name` → `Name`. Method `LoadAsync` →
`LoadCommand` (the `Async` suffix is stripped; a leading `On` is also
stripped).
See [`references/source-generators.md`](references/source-generators.md) for
the full attribute reference with generated-code samples.
---
## ViewModel patterns
### Simple observable property
```csharp
public partial class ContactViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}
```
### Hooks: `OnXxxChanging` / `OnXxxChanged`
```csharp
[ObservableProperty]
private string? name;
partial void OnNameChanged(string? value) =>
Logger.LogInformation("Name changed to {Name}", value);
```
Both single-arg `(value)` and two-arg `(oldValue, newValue)` overloads
are available. Implement only the ones you need; unimplemented hooks are
elided by the compiler (zero runtime cost).
### Dependent properties + dependent commands
```csharp
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? lastName;
public string FullName => $"{FirstName} {LastName}".Trim();
```
### Wrapping a non-observable model
```csharp
public sealed class ObservableUser(User user) : ObservableObject
{
public string Name
{
get => user.Name;
set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
}
}
```
Pass a static lambda (no captured state) to keep the call allocation-free.
---
## Commands
```csharp
[RelayCommand]
private void Refresh() => Items.Reset();
[RelayCommand]
private async Task LoadAsync()
{
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
```
Reach for manual `RelayCommand` / `AsyncRelayCommand` constructors only
when you must own the command's lifetime explicitly or compose it from
non-trivial sources. The attribute style covers ~95% of cases.
See [`references/relaycommand-cookbook.md`](references/relaycommand-cookbook.md)
for sync / async / cancellable / concurrency / error-surfacing recipes.
---
## Base class selection
| Base class | Use when |
|------------|---------|
| `ObservableObject` | Default. `INotifyPropertyChanged` + `INotifyPropertyChanging` + `SetProperty` overloads + `SetPropertyAndNotifyOnCompletion` for `Task` properties |
| `ObservableValidator` | The VM needs `INotifyDataErrorInfo` (forms, settings input) |
| `ObservableRecipient` | The VM sends or receives `IMessenger` messages — see the **`mvvm-toolkit-messenger`** skill |
C# is single-inheritance: `ObservableValidator` and `ObservableRecipient`
both extend `ObservableObject`, so combining them requires composition
(e.g., inject `IMessenger` into an `ObservableValidator`).
---
## Validation
```csharp
using System.ComponentModel.DataAnnotations;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}
```
Other entry points: `TrySetProperty`, `ValidateProperty(value, name)`,
`ClearAllErrors()`, `GetErrors(propertyName)`. Custom rules support
`[CustomValidation]` methods and custom `ValidationAttribute` subclasses.
See [`references/validation.md`](references/validation.md) for the full
validator surface area.
---
## Top pitfalls
1. **Forgetting `partial`.** Class (and every enclosing type) must be
`partial`. Compile error `MVVMTK0008` / `MVVMTK0042`.
2. **PascalCase field name.** `[ObservableProperty] private string Name;`
collides with the generated property. Use `name`, `_name`, or `m_name`.
3. **`async void` on `[RelayCommand]`.** The generator only wraps
`Task`-returning methods as `IAsyncRelayCommand`. `async void` becomes
a sync `RelayCommand` and exceptions are unobserved. Always return
`Task`.
4. **Forgetting `[NotifyCanExecuteChangedFor]`.** The Save button stays
disabled even though `CanSave()` would now return `true`.
5. **Mutating the same reference held by an `[ObservableProperty]`
field.** `EqualityComparer<T>.Default` returns `true`, no notification
fires. Replace the instance instead of mutating it.
For the full diagnostic table (`MVVMTK0xxx`) and more pitfalls, see
[`references/troubleshooting.md`](references/troubleshooting.md).
---
## End-to-end mini walkthrough
A two-pane Notes app demonstrating generators + commands +
`[NotifyCanExecuteChangedFor]`:
```csharp
public sealed partial class NoteViewModel(INotesService notes,
IMessenger messenger) : ObservableRecipient(messenger)
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync()
{
Messenger.Send(new NoteSavedMessage(Filename!));
return notes.SaveAsync(Filename!, Text!);
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private Task DeleteAsync() => notes.DeleteAsync(Filename!);
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}
```
For the full sample (DI wiring, View code-behind, XAML, unit tests), see
[`references/end-to-end-walkthrough.md`](references/end-to-end-walkthrough.md).
---
## References & companion skills
| Topic | Where |
|-------|-------|
| Source generator attribute reference | [`references/source-generators.md`](references/source-generators.md) |
| RelayCommand recipes | [`references/relaycommand-cookbook.md`](references/relaycommand-cookbook.md) |
| Validation deep dive | [`references/validation.md`](references/validation.md) |
| Full Notes-app walkthrough | [`references/end-to-end-walkthrough.md`](references/end-to-end-walkthrough.md) |
| `MVVMTK0xxx` diagnostics & pitfalls | [`references/troubleshooting.md`](references/troubleshooting.md) |
| **Messenger pub/sub** | Companion skill: **`mvvm-toolkit-messenger`** |
| **`Microsoft.Extensions.DependencyInjection` wiring** | Companion skill: **`mvvm-toolkit-di`** |
External sources:
- Toolkit overview: <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/>
- WinUI MVVM Toolkit tutorial: <https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Samples: <https://github.com/CommunityToolkit/MVVM-Samples>
© github · MIT · github/awesome-copilot