--- name: wpf-mvvm description: Build and maintain WPF MVVM patterns using CommunityToolkit.Mvvm for a .NET 8 widget-host app. Use when creating ViewModels, commands, observable state, validation, view bindings, and viewmodel-first navigation behaviors. Avoid Prism and heavy region managers; keep ViewModels testable and UI-agnostic. --- # 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 ```csharp [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 ```csharp // 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 ```csharp [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 ```csharp 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) ```csharp // In Shell or navigation service public void Navigate() where TViewModel : ObservableObject { var vm = _serviceProvider.GetRequiredService(); CurrentContent = vm; // DataTemplate maps VM to View } ``` ```xaml ``` --- ## 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 ```csharp [Fact] public void Save_WhenValid_CallsService() { var mockService = new Mock(); var vm = new MyViewModel(mockService.Object); vm.Name = "Valid Name"; vm.SaveCommand.Execute(null); mockService.Verify(s => s.Save(It.IsAny()), 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.