wpf-mvvm

SKILL.md

Wpf Mvvm

Overview

Apply MVVM conventions with CommunityToolkit.Mvvm, ensuring view models are UI-agnostic and ready for the widget shell architecture.

Constraints

  • Target .NET 8
  • CommunityToolkit.Mvvm
  • ViewModel-first navigation (custom service)
  • No Prism, no heavy region manager

Definition of Done (DoD)

  • ViewModel has no WPF dependencies (Window, UserControl, etc.)
  • Commands use [RelayCommand] attribute from CommunityToolkit.Mvvm
  • Observable properties use [ObservableProperty] attribute
  • Async commands have proper cancellation support where applicable
  • ViewModel behavior is covered by unit tests
  • No Application.Current access in ViewModels
  • Services injected via constructor (not resolved in methods)

Core Patterns

ViewModel Base Classes

Base Class Use Case
ObservableObject Standard ViewModels
ObservableValidator ViewModels with validation
ObservableRecipient ViewModels needing messaging

Observable Properties

[ObservableProperty]
private string _title = "Widget";

// With property change notification to other properties
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSave))]
private string _name = "";

// With can-execute change notification
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private bool _isValid;

Commands

// Simple command
[RelayCommand]
private void Refresh() { }

// Async command with cancellation
[RelayCommand]
private async Task LoadDataAsync(CancellationToken token)
{
    Data = await _service.GetDataAsync(token);
}

// Command with can-execute
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { }

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

Async Command Patterns

[RelayCommand]
private async Task LoadAsync(CancellationToken token)
{
    IsLoading = true;
    try
    {
        Items = await _service.GetItemsAsync(token);
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        ErrorMessage = "Failed to load items";
        Log.Error(ex, "Load failed");
    }
    finally
    {
        IsLoading = false;
    }
}

Validation

public partial class SettingsViewModel : ObservableValidator
{
    [ObservableProperty]
    [NotifyDataErrorInfo]
    [Required(ErrorMessage = "Name is required")]
    [MinLength(3)]
    private string _name = "";

    [RelayCommand(CanExecute = nameof(CanSave))]
    private void Save()
    {
        ValidateAllProperties();
        if (!HasErrors)
        {
            // Proceed
        }
    }

    private bool CanSave => !HasErrors;
}

Navigation (ViewModel-First)

// In Shell or navigation service
public void Navigate<TViewModel>() where TViewModel : ObservableObject
{
    var vm = _serviceProvider.GetRequiredService<TViewModel>();
    CurrentContent = vm;  // DataTemplate maps VM to View
}
<!-- In App.xaml or Resources -->
<DataTemplate DataType="{x:Type vm:SettingsViewModel}">
    <views:SettingsView/>
</DataTemplate>

Anti-Patterns

āŒ Don't āœ… Do
Reference Window/UserControl in VM Inject INavigationService
Call Application.Current Inject IThemeService, IShellService
Create services in methods Inject via constructor
Use code-behind for business logic Keep logic in ViewModel
Expose ICommand directly Use [RelayCommand]

Testing ViewModels

[Fact]
public void Save_WhenValid_CallsService()
{
    var mockService = new Mock<IDataService>();
    var vm = new MyViewModel(mockService.Object);
    
    vm.Name = "Valid Name";
    vm.SaveCommand.Execute(null);
    
    mockService.Verify(s => s.Save(It.IsAny<Data>()), Times.Once);
}

References

  • CommunityToolkit.Mvvm documentation
  • 3SC/ViewModels/ for examples
  • async-patterns skill for async command details
  • input-validation skill for validation patterns

References

  • references/viewmodel-patterns.md for structure patterns.
  • references/commands-and-async.md for command and async guidance.
  • references/validation.md for validation recipes.
Weekly Installs
6
First Seen
Jan 24, 2026
Installed on
opencode4
claude-code4
codex4
antigravity3
windsurf3
github-copilot2