$npx -y skills add managedcode/dotnet-skills --skill mvvmImplement the Model-View-ViewModel pattern in .NET applications with proper separation of concerns, data binding, commands, and testable ViewModels using MVVM Toolkit. USE FOR: implementing UI separation with Model-View-ViewModel; using MVVM Toolkit (CommunityToolkit.Mvvm) for Vi
| 1 | # MVVM Pattern for .NET |
| 2 | |
| 3 | ## Trigger On |
| 4 | |
| 5 | - implementing UI separation with Model-View-ViewModel |
| 6 | - using MVVM Toolkit (CommunityToolkit.Mvvm) for ViewModels |
| 7 | - designing testable UI architecture |
| 8 | - handling commands, property changes, and messaging |
| 9 | - choosing between MVVM frameworks |
| 10 | |
| 11 | ## Documentation |
| 12 | |
| 13 | - [MVVM Toolkit Overview](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) |
| 14 | - [ObservableObject](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/observableobject) |
| 15 | - [RelayCommand](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/relaycommand) |
| 16 | - [Messenger](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger) |
| 17 | - [Source Generators](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview) |
| 18 | |
| 19 | ## References |
| 20 | |
| 21 | See detailed examples in the `references/` folder: |
| 22 | - [`patterns.md`](references/patterns.md) — ViewModel, command, navigation, and state patterns |
| 23 | - [`anti-patterns.md`](references/anti-patterns.md) — Common mistakes and how to fix them |
| 24 | |
| 25 | ## Core Concepts |
| 26 | |
| 27 | | Component | Responsibility | Example | |
| 28 | |-----------|---------------|---------| |
| 29 | | **Model** | Business logic and data | `Product`, `Order`, `User` | |
| 30 | | **View** | UI presentation (XAML/Razor) | `ProductPage.xaml` | |
| 31 | | **ViewModel** | UI logic and state | `ProductViewModel` | |
| 32 | |
| 33 | ## Workflow |
| 34 | |
| 35 | 1. **Keep Views dumb** — no business logic in code-behind |
| 36 | 2. **Use data binding** — connect View to ViewModel properties |
| 37 | 3. **Commands for actions** — handle user interactions via ICommand |
| 38 | 4. **Inject dependencies** — services go into ViewModel constructors |
| 39 | 5. **Test ViewModels** — they should be unit testable without UI |
| 40 | |
| 41 | ## MVVM Toolkit Setup |
| 42 | |
| 43 | ```xml |
| 44 | <PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" /> |
| 45 | ``` |
| 46 | |
| 47 | ## ViewModel Patterns |
| 48 | |
| 49 | ### Basic ViewModel with Source Generators |
| 50 | ```csharp |
| 51 | public partial class ProductViewModel(IProductService productService) : ObservableObject |
| 52 | { |
| 53 | [ObservableProperty] |
| 54 | private string _name = string.Empty; |
| 55 | |
| 56 | [ObservableProperty] |
| 57 | private decimal _price; |
| 58 | |
| 59 | [ObservableProperty] |
| 60 | [NotifyCanExecuteChangedFor(nameof(SaveCommand))] |
| 61 | private bool _isValid; |
| 62 | |
| 63 | [RelayCommand(CanExecute = nameof(CanSave))] |
| 64 | private async Task SaveAsync() |
| 65 | { |
| 66 | await productService.SaveAsync(new Product { Name = Name, Price = Price }); |
| 67 | } |
| 68 | |
| 69 | private bool CanSave() => IsValid && !string.IsNullOrEmpty(Name); |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | ### Property Changed Notifications |
| 74 | ```csharp |
| 75 | public partial class OrderViewModel : ObservableObject |
| 76 | { |
| 77 | [ObservableProperty] |
| 78 | private int _quantity; |
| 79 | |
| 80 | [ObservableProperty] |
| 81 | private decimal _unitPrice; |
| 82 | |
| 83 | // Computed property - manually notify |
| 84 | public decimal Total => Quantity * UnitPrice; |
| 85 | |
| 86 | partial void OnQuantityChanged(int value) |
| 87 | { |
| 88 | OnPropertyChanged(nameof(Total)); |
| 89 | } |
| 90 | |
| 91 | partial void OnUnitPriceChanged(decimal value) |
| 92 | { |
| 93 | OnPropertyChanged(nameof(Total)); |
| 94 | } |
| 95 | } |
| 96 | ``` |
| 97 | |
| 98 | ### Collection ViewModel |
| 99 | ```csharp |
| 100 | public partial class ProductListViewModel(IProductService productService) : ObservableObject |
| 101 | { |
| 102 | [ObservableProperty] |
| 103 | private ObservableCollection<ProductViewModel> _products = []; |
| 104 | |
| 105 | [ObservableProperty] |
| 106 | private ProductViewModel? _selectedProduct; |
| 107 | |
| 108 | [ObservableProperty] |
| 109 | private bool _isLoading; |
| 110 | |
| 111 | [RelayCommand] |
| 112 | private async Task LoadProductsAsync() |
| 113 | { |
| 114 | IsLoading = true; |
| 115 | try |
| 116 | { |
| 117 | var items = await productService.GetAllAsync(); |
| 118 | Products = new ObservableCollection<ProductViewModel>( |
| 119 | items.Select(p => new ProductViewModel(productService) |
| 120 | { |
| 121 | Name = p.Name, |
| 122 | Price = p.Price |
| 123 | })); |
| 124 | } |
| 125 | finally |
| 126 | { |
| 127 | IsLoading = false; |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | [RelayCommand] |
| 132 | private void DeleteProduct(ProductViewModel product) |
| 133 | { |
| 134 | Products.Remove(product); |
| 135 | } |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | ## Commands |
| 140 | |
| 141 | ### Async Commands with Cancellation |
| 142 | ```csharp |
| 143 | public partial class SearchViewModel : ObservableObject |
| 144 | { |
| 145 | [ObservableProperty] |
| 146 | private string _searchText = string.Empty; |
| 147 | |
| 148 | [RelayCommand(IncludeCancelCommand = true)] |
| 149 | private async Task SearchAsync(CancellationToken token) |
| 150 | { |
| 151 | await Task.Delay(500, token); // Debounce |
| 152 | // Search logic with can |