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.Currentaccess 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 examplesasync-patternsskill for async command detailsinput-validationskill for validation patterns
References
references/viewmodel-patterns.mdfor structure patterns.references/commands-and-async.mdfor command and async guidance.references/validation.mdfor validation recipes.
Weekly Installs
6
Repository
yosrbennagra/3scFirst Seen
Jan 24, 2026
Security Audits
Installed on
opencode4
claude-code4
codex4
antigravity3
windsurf3
github-copilot2