Installation

Install with CLI Recommended
gh skills-hub install mvvm-toolkit

Don't have the extension? Run gh extension install samueltauil/skills-hub first.

Download and extract to your repository:

.github/skills/mvvm-toolkit/

Extract the ZIP to .github/skills/ in your repo. The folder name must match mvvm-toolkit for Copilot to auto-discover it.

Skill Files (6)

SKILL.md 10.5 KB
---
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>
references/
end-to-end-walkthrough.md 11.1 KB
# End-to-end walkthrough: WinUI 3 Notes app

A minimal Notes app demonstrating the full MVVM Toolkit story:
`ObservableObject`/`ObservableRecipient`, `[ObservableProperty]`,
`[RelayCommand]`, `[NotifyCanExecuteChangedFor]`, `WeakReferenceMessenger`,
and `Microsoft.Extensions.DependencyInjection`.

This walkthrough mirrors the official tutorial at
<https://learn.microsoft.com/en-us/windows/apps/tutorials/winui-mvvm-toolkit/intro>.

> The same pattern works on WPF, MAUI, Uno, and Avalonia โ€” only the
> XAML, navigation, and `App` bootstrap differ.

---

## Project layout

```
MyApp/                  โ† WinUI 3 app project
  App.xaml.cs
  Views/
    AllNotesPage.xaml
    NotePage.xaml
MyApp.Shared/           โ† .NET class library โ€” ViewModels + services
  ViewModels/
    AllNotesViewModel.cs
    NoteViewModel.cs
  Services/
    INotesService.cs
    FileSystemNotesService.cs
  Messages/
    NoteSavedMessage.cs
    NoteDeletedMessage.cs
MyApp.Tests/            โ† xUnit / MSTest project โ€” VM unit tests
```

Putting ViewModels in a separate library is the recommended pattern: the
library has no UI dependencies, so VMs are unit-testable in isolation.

---

## 1. The model

Plain POCO โ€” no toolkit dependencies.

```csharp
public sealed record NoteSummary(string Filename, string Preview, DateTime LastModified);
```

---

## 2. The service

```csharp
public interface INotesService
{
    Task<IReadOnlyList<NoteSummary>> GetAllAsync();
    Task<string> LoadAsync(string filename);
    Task SaveAsync(string filename, string text);
    Task DeleteAsync(string filename);
}

public sealed class FileSystemNotesService(string rootFolder) : INotesService
{
    public async Task<IReadOnlyList<NoteSummary>> GetAllAsync()
    {
        var files = Directory.GetFiles(rootFolder, "*.txt");
        var summaries = new List<NoteSummary>(files.Length);
        foreach (var f in files)
        {
            var text = await File.ReadAllTextAsync(f);
            summaries.Add(new NoteSummary(
                Path.GetFileName(f),
                text.Length > 60 ? text[..60] : text,
                File.GetLastWriteTime(f)));
        }
        return summaries;
    }

    public Task<string> LoadAsync(string filename) =>
        File.ReadAllTextAsync(Path.Combine(rootFolder, filename));

    public Task SaveAsync(string filename, string text) =>
        File.WriteAllTextAsync(Path.Combine(rootFolder, filename), text);

    public Task DeleteAsync(string filename)
    {
        File.Delete(Path.Combine(rootFolder, filename));
        return Task.CompletedTask;
    }
}
```

---

## 3. The messages

```csharp
public sealed record NoteSavedMessage(string Filename);
public sealed record NoteDeletedMessage(string Filename);
```

---

## 4. The list view model

```csharp
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;

public sealed partial class AllNotesViewModel : ObservableRecipient,
    IRecipient<NoteSavedMessage>,
    IRecipient<NoteDeletedMessage>
{
    private readonly INotesService notes;

    public AllNotesViewModel(INotesService notes, IMessenger messenger)
        : base(messenger)
    {
        this.notes = notes;
        IsActive = true;   // start listening for messages
    }

    public ObservableCollection<NoteSummary> Notes { get; } = new();

    [RelayCommand]
    private async Task LoadAsync()
    {
        Notes.Clear();
        foreach (var n in await notes.GetAllAsync())
            Notes.Add(n);
    }

    public void Receive(NoteSavedMessage message) => _ = LoadAsync();
    public void Receive(NoteDeletedMessage message) => _ = LoadAsync();
}
```

`ObservableRecipient`'s `OnActivated` (called when `IsActive` becomes
`true`) wires up the `IRecipient<T>` handlers automatically.

---

## 5. The editor view model

```csharp
public sealed partial class NoteViewModel : ObservableRecipient
{
    private readonly INotesService notes;

    public NoteViewModel(INotesService notes, IMessenger messenger)
        : base(messenger)
    {
        this.notes = notes;
    }

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
    private string? filename;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(SaveCommand))]
    private string? text;

    [RelayCommand]
    private async Task LoadAsync(string filename)
    {
        Filename = filename;
        Text = await notes.LoadAsync(filename);
    }

    [RelayCommand(CanExecute = nameof(CanSave))]
    private async Task SaveAsync()
    {
        await notes.SaveAsync(Filename!, Text!);
        Messenger.Send(new NoteSavedMessage(Filename!));
    }

    [RelayCommand(CanExecute = nameof(CanDelete))]
    private async Task DeleteAsync()
    {
        await notes.DeleteAsync(Filename!);
        Messenger.Send(new NoteDeletedMessage(Filename!));
        Filename = null;
        Text = null;
    }

    private bool CanSave() =>
        !string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);

    private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}
```

---

## 6. The composition root (`App.xaml.cs`)

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using CommunityToolkit.Mvvm.Messaging;

public partial class App : Application
{
    public IHost Host { get; }

    public App()
    {
        InitializeComponent();

        var notesRoot = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "MyApp", "notes");
        Directory.CreateDirectory(notesRoot);

        Host = Microsoft.Extensions.Hosting.Host
            .CreateDefaultBuilder()
            .ConfigureServices((_, services) =>
            {
                services.AddSingleton<INotesService>(_ => new FileSystemNotesService(notesRoot));
                services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);

                services.AddSingleton<AllNotesViewModel>();
                services.AddTransient<NoteViewModel>();
            })
            .Build();
    }

    public static T GetService<T>() where T : class =>
        ((App)Current).Host.Services.GetRequiredService<T>();
}
```

---

## 7. Wire up the views

`AllNotesPage.xaml.cs`:

```csharp
public sealed partial class AllNotesPage : Page
{
    public AllNotesViewModel ViewModel { get; } = App.GetService<AllNotesViewModel>();

    public AllNotesPage()
    {
        InitializeComponent();
    }

    protected override async void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        await ViewModel.LoadCommand.ExecuteAsync(null);
    }
}
```

`AllNotesPage.xaml`:

```xml
<Page x:Class="MyApp.Views.AllNotesPage"
      xmlns:vm="using:MyApp.Shared.ViewModels">
    <Grid RowDefinitions="Auto,*">
        <CommandBar>
            <AppBarButton Icon="Add" Label="New" Click="OnNewClicked"/>
            <AppBarButton Icon="Refresh" Label="Refresh"
                          Command="{x:Bind ViewModel.LoadCommand}"/>
        </CommandBar>
        <ListView Grid.Row="1"
                  ItemsSource="{x:Bind ViewModel.Notes}"
                  ItemClick="OnNoteClicked"
                  IsItemClickEnabled="True">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="vm:NoteSummary">
                    <StackPanel>
                        <TextBlock Text="{x:Bind Filename}" FontWeight="SemiBold"/>
                        <TextBlock Text="{x:Bind Preview}"
                                   TextTrimming="CharacterEllipsis"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>
```

`NotePage.xaml.cs`:

```csharp
public sealed partial class NotePage : Page
{
    public NoteViewModel ViewModel { get; } = App.GetService<NoteViewModel>();

    public NotePage()
    {
        InitializeComponent();
    }

    protected override async void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
        if (e.Parameter is string filename)
            await ViewModel.LoadCommand.ExecuteAsync(filename);
        ViewModel.IsActive = true;
    }

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

`NotePage.xaml`:

```xml
<Page x:Class="MyApp.Views.NotePage">
    <Grid RowDefinitions="Auto,*,Auto">
        <TextBox Header="Filename" Text="{x:Bind ViewModel.Filename, Mode=TwoWay}"/>
        <TextBox Grid.Row="1"
                 AcceptsReturn="True" TextWrapping="Wrap"
                 Text="{x:Bind ViewModel.Text, Mode=TwoWay}"/>
        <StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
            <Button Content="Save"  Command="{x:Bind ViewModel.SaveCommand}"/>
            <Button Content="Delete" Command="{x:Bind ViewModel.DeleteCommand}"/>
        </StackPanel>
    </Grid>
</Page>
```

---

## 8. A representative unit test

```csharp
using CommunityToolkit.Mvvm.Messaging;

public sealed class NoteViewModelTests
{
    private sealed class FakeNotesService : INotesService
    {
        public List<(string filename, string text)> Saved { get; } = new();
        public Task<IReadOnlyList<NoteSummary>> GetAllAsync() => Task.FromResult<IReadOnlyList<NoteSummary>>(Array.Empty<NoteSummary>());
        public Task<string> LoadAsync(string filename) => Task.FromResult(string.Empty);
        public Task SaveAsync(string filename, string text)
        {
            Saved.Add((filename, text));
            return Task.CompletedTask;
        }
        public Task DeleteAsync(string filename) => Task.CompletedTask;
    }

    [Fact]
    public async Task SaveCommand_persists_and_broadcasts()
    {
        var notes = new FakeNotesService();
        var messenger = new WeakReferenceMessenger();
        string? receivedFilename = null;
        messenger.Register<NoteSavedMessage>(new object(), (_, m) => receivedFilename = m.Filename);

        var vm = new NoteViewModel(notes, messenger)
        {
            Filename = "hello.txt",
            Text = "world"
        };

        await vm.SaveCommand.ExecuteAsync(null);

        Assert.Single(notes.Saved);
        Assert.Equal("hello.txt", notes.Saved[0].filename);
        Assert.Equal("world", notes.Saved[0].text);
        Assert.Equal("hello.txt", receivedFilename);
    }
}
```

---

## What to internalize from this sample

1. **VMs go in a UI-free class library.** The toolkit's only dependency
   is `netstandard2.0+`, so VMs are testable without a UI host.
2. **Constructor injection everywhere.** The composition root knows how
   to build everything; ViewModels and services receive their
   dependencies via parameters.
3. **`IMessenger` is the cross-VM glue.** `WeakReferenceMessenger.Default`
   is the right default. The list VM listens via `IRecipient<T>`; the
   editor VM publishes via `Messenger.Send`.
4. **`[NotifyCanExecuteChangedFor]` keeps Save/Delete buttons in sync**
   with text input โ€” no manual wiring needed.
5. **`ObservableRecipient.IsActive`** controls subscription lifetime โ€”
   set it from `OnNavigatedTo` / `OnNavigatedFrom` (or an equivalent
   activation hook in your framework).
relaycommand-cookbook.md 6.7 KB
# RelayCommand cookbook

Recipes for `RelayCommand` / `AsyncRelayCommand` and the `[RelayCommand]`
generator. Defaults to the generator-attribute style; manual constructor
patterns are listed at the bottom for advanced cases.

---

## Sync command

```csharp
[RelayCommand]
private void IncrementCounter() => Counter++;
```

```xml
<Button Command="{x:Bind ViewModel.IncrementCounterCommand}" Content="+1"/>
```

## Sync command with parameter

```csharp
[RelayCommand]
private void RemoveItem(Item item) => Items.Remove(item);
```

```xml
<Button Command="{x:Bind ViewModel.RemoveItemCommand}"
        CommandParameter="{x:Bind Item}" Content="Remove"/>
```

The generator picks `IRelayCommand<Item>` based on the parameter type.

## Sync command with `CanExecute`

```csharp
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? text;

[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit() => service.Submit(Text!);

private bool CanSubmit() => !string.IsNullOrWhiteSpace(Text);
```

`[NotifyCanExecuteChangedFor]` raises `CanExecuteChanged` automatically
whenever `Text` changes โ€” without it, the button stays disabled even after
the user types.

---

## Async command

```csharp
[RelayCommand]
private async Task LoadAsync()
{
    Items.Clear();
    foreach (var item in await service.GetItemsAsync())
        Items.Add(item);
}
```

Bind the UI to `LoadCommand.IsRunning` to show a spinner:

```xml
<ProgressRing IsActive="{x:Bind ViewModel.LoadCommand.IsRunning, Mode=OneWay}"/>
```

## Async command with cancellation

```csharp
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
    try
    {
        await using var stream = await http.GetStreamAsync(url, token);
        // ...
    }
    catch (OperationCanceledException)
    {
        // Expected โ€” user cancelled.
    }
}
```

```xml
<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>
```

`DownloadCancelCommand.CanExecute` is automatically wired to
`DownloadCommand.IsRunning`.

## Async command with concurrency

```csharp
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync(string host)
{
    await pingService.PingAsync(host);
}
```

Default (`AllowConcurrentExecutions = false`) reports the command as
disabled while a previous execution is pending. Set to `true` for
fire-and-forget patterns where overlapping invocations are safe.

## Async command that surfaces errors to UI

```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task SyncAsync(CancellationToken token)
{
    await syncService.SyncAsync(token);
}
```

```xml
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Exception, Mode=OneWay}"/>
```

Without `FlowExceptionsToTaskScheduler = true`, an uncaught exception in
`SyncAsync` will crash the app (mirroring sync commands). With it, the
exception is surfaced through `ExecutionTask` and bubbles to
`TaskScheduler.UnobservedTaskException`.

## Showing async command status

```xml
<StackPanel>
    <ProgressRing IsActive="{x:Bind ViewModel.SyncCommand.IsRunning, Mode=OneWay}"/>
    <TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Status, Mode=OneWay}"/>
</StackPanel>
```

Useful properties on `IAsyncRelayCommand`:

| Property | Type | Purpose |
|----------|------|---------|
| `ExecutionTask` | `Task?` | The currently running (or last completed) task |
| `IsRunning` | `bool` | `true` while a task is in flight |
| `CanBeCanceled` | `bool` | `true` if the wrapped method takes a `CancellationToken` |
| `IsCancellationRequested` | `bool` | `true` after `Cancel()` was called for the in-flight task |

Methods:

| Method | Purpose |
|--------|---------|
| `Cancel()` | Signals the active `CancellationToken` |
| `NotifyCanExecuteChanged()` | Re-evaluates `CanExecute` and raises `CanExecuteChanged` |

---

## Forwarding attributes to the generated command property

```csharp
[RelayCommand]
[property: JsonIgnore]
[property: Description("Saves the current document")]
private Task SaveAsync() => repo.SaveAsync(Text!);
```

The generator emits `SaveCommand` with `[JsonIgnore]` and `[Description]`
applied โ€” useful when the VM is serialized.

---

## Manual `RelayCommand` / `AsyncRelayCommand`

Reach for the manual constructors when you need:

- A command composed from multiple methods or dynamically rebuilt
- A `CanExecute` predicate built from external observables
- An ICommand instance held in a field (rare; the generator's lazy property
  is enough for almost every case)

```csharp
public sealed class CounterViewModel : ObservableObject
{
    public CounterViewModel()
    {
        IncrementCommand = new RelayCommand(() => Counter++);
        DecrementCommand = new RelayCommand(() => Counter--, () => Counter > 0);
    }

    [ObservableProperty]
    private int counter;

    public IRelayCommand IncrementCommand { get; }
    public IRelayCommand DecrementCommand { get; }
}
```

```csharp
public sealed class DownloadViewModel : ObservableObject
{
    public DownloadViewModel()
    {
        DownloadCommand = new AsyncRelayCommand(DownloadAsync, () => CanDownload);
    }

    [ObservableProperty]
    private bool canDownload = true;

    public IAsyncRelayCommand DownloadCommand { get; }

    private async Task DownloadAsync()
    {
        CanDownload = false;
        try { await http.DownloadAsync(); }
        finally { CanDownload = true; }
    }
}
```

Trigger `CanExecute` re-evaluation manually with
`SomeCommand.NotifyCanExecuteChanged()`.

---

## `Task.WhenAll` from a single command

```csharp
[RelayCommand]
private async Task SyncAllAsync(CancellationToken token)
{
    var tasks = providers.Select(p => p.SyncAsync(token));
    await Task.WhenAll(tasks);
}
```

If you want individual progress tracking per provider, expose one command
per provider instead.

---

## Common mistakes

1. **`async void` instead of `async Task`.** The generator only wraps
   `Task`-returning methods as `IAsyncRelayCommand`. `async void` becomes a
   sync `RelayCommand` and exceptions are unobserved.
2. **Forgetting `[NotifyCanExecuteChangedFor]`.** The button stays disabled
   even though `CanX()` would now return `true`.
3. **Calling `Cancel()` on a non-cancellable command.** Only commands whose
   wrapped method accepts a `CancellationToken` honor `Cancel()`.
4. **Catching `OperationCanceledException` and rethrowing as a different
   type.** Loses cancellation semantics; `ExecutionTask.IsCanceled` will be
   `false`. Let `OperationCanceledException` propagate (or return).
5. **Awaiting `IAsyncRelayCommand.ExecuteAsync()` from inside another
   `[RelayCommand]`.** Prefer calling the underlying method directly to
   avoid double-wrapping the cancellation/concurrency semantics.
source-generators.md 9.3 KB
# Source generators reference

Complete attribute reference for `CommunityToolkit.Mvvm` 8.x source
generators, with the code each one produces.

> **Universal rule.** Every type that uses one of these attributes โ€” and
> every enclosing type, if nested โ€” must be declared `partial`. The
> generators emit a sibling partial class declaration; without `partial`,
> the compiler reports `MVVMTK0008` / `MVVMTK0042`.

---

## `[ObservableProperty]`

Generates an observable property from a private field.

```csharp
using CommunityToolkit.Mvvm.ComponentModel;

public partial class SampleViewModel : ObservableObject
{
    [ObservableProperty]
    private string? name;
}
```

Generated (simplified):

```csharp
public string? Name
{
    get => name;
    set
    {
        if (!EqualityComparer<string?>.Default.Equals(name, value))
        {
            string? oldValue = name;
            OnNameChanging(value);
            OnNameChanging(oldValue, value);
            OnPropertyChanging();
            name = value;
            OnNameChanged(value);
            OnNameChanged(oldValue, value);
            OnPropertyChanged();
        }
    }
}

partial void OnNameChanging(string? value);
partial void OnNameChanging(string? oldValue, string? newValue);
partial void OnNameChanged(string? value);
partial void OnNameChanged(string? oldValue, string? newValue);
```

### Naming

- Field `name` โ†’ property `Name`
- Field `_name` โ†’ property `Name`
- Field `m_name` โ†’ property `Name`
- Field `Name` (PascalCase) โ†’ **error** (collides with generated property)

### Hooks

Implement any subset of the partial methods. Unimplemented hooks are
elided by the compiler โ€” zero runtime cost.

```csharp
[ObservableProperty]
private ChildViewModel? selectedItem;

partial void OnSelectedItemChanging(ChildViewModel? oldValue, ChildViewModel? newValue)
{
    if (oldValue is not null) oldValue.IsSelected = false;
    if (newValue is not null) newValue.IsSelected = true;
}
```

The hook methods are `partial` with no body declaration โ€” you cannot add
an explicit accessibility (no `public`/`private`).

---

## `[NotifyPropertyChangedFor(nameof(Other))]`

Raises `PropertyChanged` for additional properties when this field changes.
Stack multiple attributes for multiple targets.

```csharp
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyPropertyChangedFor(nameof(Initials))]
private string? firstName;
```

Use it for derived/computed properties:

```csharp
public string FullName => $"{FirstName} {LastName}";
public string Initials => $"{FirstName?[0]}{LastName?[0]}";
```

---

## `[NotifyCanExecuteChangedFor(nameof(MyCommand))]`

Calls `MyCommand.NotifyCanExecuteChanged()` when this field changes. The
target must be an `IRelayCommand` (or `IAsyncRelayCommand`) property.

```csharp
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? name;

[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);

private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
```

> **`MVVMTK0016`** is raised if the target is not an accessible
> `IRelayCommand` property in the same type.

---

## `[NotifyDataErrorInfo]`

Only valid in types that inherit from `ObservableValidator`. Adds a
`ValidateProperty(value)` call inside the generated setter, so DataAnnotation
validators run on every assignment.

```csharp
using System.ComponentModel.DataAnnotations;

public partial class RegistrationViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required, MinLength(2), MaxLength(100)]
    private string? name;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required, EmailAddress]
    private string? email;
}
```

Only attributes that derive from `ValidationAttribute` are forwarded to the
generated property. Other attributes are ignored unless you use
`[property: ]` (see below).

---

## `[NotifyPropertyChangedRecipients]`

Only valid in types that inherit from `ObservableRecipient`. Adds a
`Broadcast(oldValue, newValue)` call after a successful set, sending a
`PropertyChangedMessage<T>` to all recipients of the active `IMessenger`.

```csharp
public partial class SelectionViewModel : ObservableRecipient
{
    [ObservableProperty]
    [NotifyPropertyChangedRecipients]
    private Item? selectedItem;
}
```

Subscribers can listen with:

```csharp
WeakReferenceMessenger.Default.Register<SelectionViewModel, PropertyChangedMessage<Item>>(
    this,
    static (r, m) =>
    {
        if (m.PropertyName == nameof(SelectionViewModel.SelectedItem))
            r.Handle(m.NewValue);
    });
```

---

## `[RelayCommand]`

Generates a lazy `RelayCommand` / `AsyncRelayCommand` from an instance
method. Exposes it via the `IRelayCommand` / `IAsyncRelayCommand` interface.

```csharp
[RelayCommand]
private void Refresh() => Items.Reset();
```

```csharp
private RelayCommand? refreshCommand;
public IRelayCommand RefreshCommand =>
    refreshCommand ??= new RelayCommand(Refresh);
```

### Naming

- `Refresh` โ†’ `RefreshCommand`
- `OnRefresh` โ†’ `RefreshCommand` (leading `On` stripped)
- `LoadAsync` โ†’ `LoadCommand` (trailing `Async` stripped)
- `OnLoadAsync` โ†’ `LoadCommand` (both stripped)

### Sync with parameter

```csharp
[RelayCommand]
private void GreetUser(User user) => Console.WriteLine($"Hello {user.Name}");
```

Generates `IRelayCommand<User> GreetUserCommand` (a typed command).

### Async without cancellation

```csharp
[RelayCommand]
private async Task GreetUserAsync()
{
    var user = await users.GetCurrentAsync();
    Console.WriteLine($"Hello {user.Name}");
}
```

Generates `IAsyncRelayCommand GreetUserCommand` backed by
`AsyncRelayCommand`.

### Async with cancellation

```csharp
[RelayCommand]
private async Task GreetUserAsync(CancellationToken token)
{
    try
    {
        var user = await users.GetCurrentAsync(token);
        Console.WriteLine($"Hello {user.Name}");
    }
    catch (OperationCanceledException) { /* expected */ }
}
```

The toolkit propagates a `CancellationToken` to the wrapped method. Calling
`GreetUserCommand.Cancel()` signals it.

### `IncludeCancelCommand = true`

Generates a paired `XxxCancelCommand` whose `CanExecute` is wired to the
underlying async command's `IsRunning` state โ€” bind it to a Cancel button:

```csharp
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token) { /* ... */ }
```

```xml
<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>
```

### `CanExecute = nameof(MethodOrProperty)`

```csharp
[RelayCommand(CanExecute = nameof(CanGreetUser))]
private void GreetUser(User? user) => Console.WriteLine($"Hello {user!.Name}");

private bool CanGreetUser(User? user) => user is not null;
```

The `CanExecute` member is invoked initially when the command is bound, and
again every time the command's `NotifyCanExecuteChanged` runs (use
`[NotifyCanExecuteChangedFor]` to wire that automatically when bound state
changes).

### `AllowConcurrentExecutions = true`

Default is `false`: while an invocation is pending, the command reports
itself as not executable. Setting `true` allows queued/parallel invocations.

```csharp
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync() { /* fire-and-keep-going */ }
```

When the wrapped method takes a `CancellationToken` and concurrent execution
is **not** allowed, requesting a new execution while one is pending cancels
the prior token first.

### `FlowExceptionsToTaskScheduler = true`

Default is await-and-rethrow (exceptions crash the app, mirroring sync
commands). Setting `true` routes exceptions through `ExecutionTask` and
`TaskScheduler.UnobservedTaskException` instead โ€” useful when the UI binds
to `ExecutionTask.Status` to render error states.

```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }
```

---

## `[property: SomeAttribute(...)]`

Forwards an attribute onto the generated property (for either
`[ObservableProperty]` fields or `[RelayCommand]` methods).

```csharp
[ObservableProperty]
[property: JsonRequired]
[property: JsonPropertyName("name")]
private string? username;

[RelayCommand]
[property: JsonIgnore]
private void GreetUser(User user) { /* ... */ }
```

Use this for serialization attributes (`[JsonIgnore]`,
`[JsonPropertyName]`, `[XmlElement]`), data attributes (`[Display(Name=...)]`),
or any other attribute that needs to live on the property/command instead of
on the field/method.

---

## `[INotifyPropertyChanged]` (class-level)

Use only when you can't inherit from `ObservableObject` (e.g., the type
already inherits from a different base). Generates the
`INotifyPropertyChanged` plumbing on the type itself.

```csharp
using CommunityToolkit.Mvvm.ComponentModel;

[INotifyPropertyChanged]
public partial class MyControl : UserControl
{
    [ObservableProperty]
    private string? caption;
}
```

Prefer `ObservableObject` (or `ObservableValidator` /
`ObservableRecipient`) inheritance whenever possible. The class-level
attribute exists primarily for inheritance-locked scenarios such as
custom controls and platform base types.

There is also `[ObservableObject]` (class-level) for the same purpose if
you want the full `SetProperty<T>` API surface generated onto the type
without inheritance.
troubleshooting.md 7.0 KB
# Troubleshooting

Common errors, diagnostics, and gotchas with `CommunityToolkit.Mvvm` 8.x.

---

## Source-generator diagnostics (`MVVMTK0xxx`)

The generators emit numbered diagnostics. The most common ones:

| Code | Meaning | Fix |
|------|---------|-----|
| `MVVMTK0008` | The containing type (or an enclosing type) is not `partial` | Add `partial` to the class declaration **and** every enclosing type |
| `MVVMTK0016` | `[NotifyCanExecuteChangedFor]` target is not an accessible `IRelayCommand` property | Make sure the target is a `[RelayCommand]`-generated command (or a manually declared `IRelayCommand` property) on the same type |
| `MVVMTK0017` | `[NotifyDataErrorInfo]` used outside `ObservableValidator` | Inherit from `ObservableValidator` or remove the attribute |
| `MVVMTK0018` | `[NotifyPropertyChangedRecipients]` used outside `ObservableRecipient` | Inherit from `ObservableRecipient` or remove the attribute |
| `MVVMTK0030` | `[ObservableProperty]` used in a type that does not implement `INotifyPropertyChanged` (and the class-level `[INotifyPropertyChanged]` / `[ObservableObject]` attributes are also missing) | Inherit from `ObservableObject` or apply `[INotifyPropertyChanged]` / `[ObservableObject]` to the type |
| `MVVMTK0042` | The `[ObservableProperty]` field belongs to a generic type without proper `partial` declarations | Same fix as `MVVMTK0008` (add `partial`) |

Search the full table at:
<https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>

---

## "Property name collides with field name"

```text
'SampleViewModel' already contains a definition for 'Name'
```

You named the field with PascalCase:

```csharp
[ObservableProperty]
private string Name;   // โŒ collides with generated property
```

Use lowerCamel (or prefixed) instead:

```csharp
[ObservableProperty]
private string? name;   // โœ… generates Name
```

---

## "Setter never raises `PropertyChanged`"

Possible causes:

1. **Same reference assigned.** The generator uses
   `EqualityComparer<T>.Default.Equals` to detect changes. For reference
   types where you mutated the same instance, the comparer returns `true`
   and notification is skipped. Replace the instance instead of mutating.
2. **Property set to identical value.** Same value โ†’ no notification by
   design.
3. **Custom comparer needed.** For value types where default equality is
   wrong, write the property by hand and call
   `SetProperty(ref field, value, comparer)`.

---

## "ContentDialog throws `InvalidOperationException`" (WinUI 3)

Not a toolkit issue, but commonly hit from `[RelayCommand]` async methods.
Set `XamlRoot` before calling `ShowAsync()`. See the
`winui3-migration-guide` skill for details.

---

## Async `[RelayCommand]` swallows exceptions

Default behavior: the wrapped task is awaited and the exception is
rethrown on the synchronization context. If your method is `async void`,
the generator wraps it as a sync `RelayCommand` and exceptions become
unobserved. **Always return `Task` from `[RelayCommand]` methods.**

If the UI binds to `ExecutionTask.Exception` to render errors, opt into
`FlowExceptionsToTaskScheduler = true`:

```csharp
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task LoadAsync(CancellationToken token) { /* ... */ }
```

---

## Cancellation appears to do nothing

- Ensure the wrapped method declares a `CancellationToken` parameter.
- Pass the token down to the awaited APIs (`HttpClient.GetAsync(url, token)`,
  `Task.Delay(ms, token)`, etc.).
- Catch `OperationCanceledException` so the UI doesn't see an error.

---

## Messenger handler never fires

Checklist:

1. The recipient is registered for the **exact** message type, not a base
   type. Inheritance is **not** considered.
2. The same `IMessenger` instance is used to send and register
   (`WeakReferenceMessenger.Default` vs an injected per-window messenger).
3. The token (channel) matches between sender and receiver.
4. With `WeakReferenceMessenger`, the recipient might already have been
   garbage-collected. Hold a strong reference somewhere (typically the DI
   container does this for singleton VMs).
5. With `ObservableRecipient`, `IsActive` must be `true` โ€” `OnActivated`
   is what registers the `IRecipient<T>` handlers.

---

## `OnActivated` never runs

`ObservableRecipient.OnActivated` is invoked when `IsActive` flips from
`false` to `true`. If you never set `IsActive = true`, no handlers register.
Common pattern:

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

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

---

## Memory leak with `StrongReferenceMessenger`

Strong-ref recipients are pinned until you call `Unregister`. Either:

- Inherit from `ObservableRecipient` (auto-unregisters in `OnDeactivated`).
- Switch to `WeakReferenceMessenger.Default`.
- Call `messenger.UnregisterAll(this)` in your dispose / tear-down path.

---

## "Cannot inherit from `ObservableValidator` and `ObservableRecipient`"

C# single inheritance โ€” pick one. If you need both:

- Inherit from `ObservableRecipient` (or `ObservableValidator`).
- Inject `IMessenger` (or implement validation) on the side via
  composition.

Or use the class-level `[INotifyPropertyChanged]` / `[ObservableObject]`
attribute on a custom base type that wraps both pieces.

---

## DI container can't construct ViewModel

Symptom: `InvalidOperationException` mentioning "Unable to resolve service
for type 'X' while attempting to activate 'MyViewModel'".

Causes:

- Constructor parameter type wasn't registered. Add `services.AddX(...)`.
- Multiple ambiguous constructors โ€” the container picks the longest one
  whose dependencies are all registered. If two constructors qualify, an
  exception is thrown. Mark one as the canonical constructor or remove the
  ambiguity.
- Scoped service injected into a singleton (in dev mode with scope
  validation). Either change the lifetime or inject `IServiceScopeFactory`
  and resolve from a scope.

---

## XAML cannot resolve namespace

```text
The type 'local:ContactViewModel' was not found.
```

XAML namespace mappings need the assembly to be referenced and the
namespace to match. If the VM lives in a class library, the mapping needs
the assembly name:

```xml
xmlns:vm="using:MyApp.Shared.ViewModels;assembly=MyApp.Shared"
```

(WPF syntax differs slightly: `xmlns:vm="clr-namespace:...;assembly=..."`.)

---

## "Design-time data shows nothing"

Design-time XAML editors instantiate the page without your DI container.
Either:

- Provide a parameterless constructor that bootstraps a design-time VM.
- Use `d:DataContext="{d:DesignInstance Type=vm:ContactViewModel, IsDesignTimeCreatable=True}"`.
- Use a separate design-time view model class with hard-coded sample data.

---

## More

- All `MVVMTK0xxx` errors:
  <https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/errors/>
- Source: <https://github.com/CommunityToolkit/dotnet>
- Sample app: <https://aka.ms/mvvmtoolkit/samples>
validation.md 5.7 KB
# Validation with `ObservableValidator`

`ObservableValidator` extends `ObservableObject` with `INotifyDataErrorInfo`
support, integrating with
`System.ComponentModel.DataAnnotations` validation attributes.

---

## Quick start

```csharp
using System.ComponentModel.DataAnnotations;
using CommunityToolkit.Mvvm.ComponentModel;

public sealed partial class RegistrationViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required]
    [MinLength(2), MaxLength(100)]
    private string? name;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required, EmailAddress]
    private string? email;

    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Range(13, 120)]
    private int age;

    [RelayCommand]
    private void Submit()
    {
        ValidateAllProperties();
        if (HasErrors) return;
        // submit...
    }
}
```

`[NotifyDataErrorInfo]` makes the generated setter call
`ValidateProperty(value)` after each successful set, so validation runs as
the user types.

---

## Manual `SetProperty` validation

If you write the property by hand instead of using `[ObservableProperty]`,
opt into validation with the `bool validate` parameter:

```csharp
[Required, MinLength(2), MaxLength(100)]
public string? Name
{
    get => name;
    set => SetProperty(ref name, value, validate: true);
}
```

---

## `TrySetProperty`

Sometimes you want to set a property only if validation succeeds:

```csharp
[Required, EmailAddress]
public string? Email
{
    get => email;
    set
    {
        if (TrySetProperty(ref email, value, out IReadOnlyCollection<ValidationResult> errors))
        {
            // value passed validation; success
        }
        else
        {
            // inspect errors
        }
    }
}
```

---

## `ValidateAllProperties()`

Forces validation across every public property in the type that has at
least one `ValidationAttribute`. Call before submission:

```csharp
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
    ValidateAllProperties();
    if (HasErrors) return;
    submitter.Submit(this);
}

private bool CanSubmit() => !HasErrors;
```

Pair with `[NotifyCanExecuteChangedFor]` on the input fields, plus a
listener on `ErrorsChanged` (or override `OnErrorsChanged`) to keep the
button state in sync as the user types.

---

## `ValidateProperty(value, propertyName)`

Trigger validation manually for one property โ€” useful when validation of
property `A` depends on property `B`:

```csharp
[Range(20, 80)]
[ObservableProperty]
private int b;

[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;

partial void OnBChanged(int value)
{
    // Re-run A's validation since it depends on B.
    ValidateProperty(A, nameof(A));
}
```

---

## `ClearAllErrors()`

Reset the error state โ€” common after a successful submit or when resetting
a form:

```csharp
[RelayCommand]
private void Reset()
{
    Name = null;
    Email = null;
    Age = 0;
    ClearAllErrors();
}
```

---

## Custom validation method (`[CustomValidation]`)

```csharp
[Required, MinLength(3)]
[CustomValidation(typeof(RegistrationViewModel), nameof(ValidateUsername))]
[ObservableProperty]
private string? username;

public static ValidationResult ValidateUsername(string? value, ValidationContext context)
{
    var vm = (RegistrationViewModel)context.ObjectInstance;
    if (vm.userService.IsTaken(value!))
        return new ValidationResult("Username is already taken.");
    return ValidationResult.Success!;
}
```

The method must be `static` and accept `(value, ValidationContext)`. Use
`context.ObjectInstance` to reach back into the ViewModel.

---

## Custom `ValidationAttribute`

For reusable rules, subclass `ValidationAttribute`:

```csharp
public sealed class GreaterThanAttribute(string otherPropertyName)
    : ValidationAttribute
{
    public string OtherPropertyName { get; } = otherPropertyName;

    protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
    {
        var instance = ctx.ObjectInstance;
        var other = instance.GetType().GetProperty(OtherPropertyName)?.GetValue(instance);
        if (((IComparable)value!).CompareTo(other) > 0)
            return ValidationResult.Success;
        return new ValidationResult($"Must be greater than {OtherPropertyName}.");
    }
}
```

Apply to the property:

```csharp
[Range(10, 100)]
[GreaterThan(nameof(B))]
[ObservableProperty]
private int a;
```

---

## Reading errors in the View

`ObservableValidator` implements `INotifyDataErrorInfo`. XAML stacks render
`ErrorsChanged` automatically when `ValidatesOnNotifyDataErrors=True` (WPF)
or via control templates (WinUI 3, MAUI). To inspect errors in code:

```csharp
foreach (ValidationResult result in vm.GetErrors(nameof(vm.Name)))
{
    Console.WriteLine(result.ErrorMessage);
}

// Across all properties
foreach (ValidationResult result in vm.GetErrors())
{
    Console.WriteLine(result.ErrorMessage);
}

bool any = vm.HasErrors;
```

Subscribe to changes:

```csharp
vm.ErrorsChanged += (s, e) =>
{
    Debug.WriteLine($"Errors changed for {e.PropertyName}");
};
```

---

## Tips

- Combine `ValidateAllProperties()` with `[NotifyCanExecuteChangedFor]` so
  the Submit button reflects validity in real time.
- Keep validation rules in the ViewModel (or via custom attributes), not
  in the model โ€” the model should be a plain DTO.
- For network or async validation (e.g., "is username taken?"), use
  `[CustomValidation]` calling a synchronous wrapper around an async lookup
  (or perform the async check separately and surface the result via
  `AddError(propertyName, ...)`-style helpers if you write your own).
- `ObservableValidator` cannot also inherit from `ObservableRecipient` โ€”
  if you need messaging, inject `IMessenger` and call `Send` directly.

License (MIT)

View full license text
MIT License

Copyright GitHub, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.