--- name: mvvm description: "Implement 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 ViewModels; designing testable UI architecture. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made." compatibility: "Applies to WPF, MAUI, WinUI, Uno Platform, Avalonia, and Blazor projects." --- # MVVM Pattern for .NET ## Trigger On - implementing UI separation with Model-View-ViewModel - using MVVM Toolkit (CommunityToolkit.Mvvm) for ViewModels - designing testable UI architecture - handling commands, property changes, and messaging - choosing between MVVM frameworks ## Documentation - [MVVM Toolkit Overview](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) - [ObservableObject](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/observableobject) - [RelayCommand](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/relaycommand) - [Messenger](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/messenger) - [Source Generators](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/generators/overview) ## References See detailed examples in the `references/` folder: - [`patterns.md`](references/patterns.md) — ViewModel, command, navigation, and state patterns - [`anti-patterns.md`](references/anti-patterns.md) — Common mistakes and how to fix them ## Core Concepts | Component | Responsibility | Example | |-----------|---------------|---------| | **Model** | Business logic and data | `Product`, `Order`, `User` | | **View** | UI presentation (XAML/Razor) | `ProductPage.xaml` | | **ViewModel** | UI logic and state | `ProductViewModel` | ## Workflow 1. **Keep Views dumb** — no business logic in code-behind 2. **Use data binding** — connect View to ViewModel properties 3. **Commands for actions** — handle user interactions via ICommand 4. **Inject dependencies** — services go into ViewModel constructors 5. **Test ViewModels** — they should be unit testable without UI ## MVVM Toolkit Setup ```xml ``` ## ViewModel Patterns ### Basic ViewModel with Source Generators ```csharp public partial class ProductViewModel(IProductService productService) : ObservableObject { [ObservableProperty] private string _name = string.Empty; [ObservableProperty] private decimal _price; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private bool _isValid; [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { await productService.SaveAsync(new Product { Name = Name, Price = Price }); } private bool CanSave() => IsValid && !string.IsNullOrEmpty(Name); } ``` ### Property Changed Notifications ```csharp public partial class OrderViewModel : ObservableObject { [ObservableProperty] private int _quantity; [ObservableProperty] private decimal _unitPrice; // Computed property - manually notify public decimal Total => Quantity * UnitPrice; partial void OnQuantityChanged(int value) { OnPropertyChanged(nameof(Total)); } partial void OnUnitPriceChanged(decimal value) { OnPropertyChanged(nameof(Total)); } } ``` ### Collection ViewModel ```csharp public partial class ProductListViewModel(IProductService productService) : ObservableObject { [ObservableProperty] private ObservableCollection _products = []; [ObservableProperty] private ProductViewModel? _selectedProduct; [ObservableProperty] private bool _isLoading; [RelayCommand] private async Task LoadProductsAsync() { IsLoading = true; try { var items = await productService.GetAllAsync(); Products = new ObservableCollection( items.Select(p => new ProductViewModel(productService) { Name = p.Name, Price = p.Price })); } finally { IsLoading = false; } } [RelayCommand] private void DeleteProduct(ProductViewModel product) { Products.Remove(product); } } ``` ## Commands ### Async Commands with Cancellation ```csharp public partial class SearchViewModel : ObservableObject { [ObservableProperty] private string _searchText = string.Empty; [RelayCommand(IncludeCancelCommand = true)] private async Task SearchAsync(CancellationToken token) { await Task.Delay(500, token); // Debounce // Search logic with cancellation support } } ``` ### Command with Parameter ```csharp public partial class NavigationViewModel : ObservableObject { [RelayCommand] private void NavigateTo(string page) { // Navigate to page } [RelayCommand] private async Task OpenItemAsync(int itemId) { // Load and open item } } ``` ## Messenger Pattern ### Sending Messages ```csharp // Define message public record ProductSelectedMessage(Product Product); // Send from one ViewModel WeakReferenceMessenger.Default.Send(new ProductSelectedMessage(selectedProduct)); ``` ### Receiving Messages ```csharp public partial class ProductDetailViewModel : ObservableRecipient { public ProductDetailViewModel() { IsActive = true; // Enable message reception } protected override void OnActivated() { Messenger.Register( this, (r, m) => r.LoadProduct(m.Product)); } private void LoadProduct(Product product) { // Update UI with product details } } ``` ## Validation ### Using ObservableValidator ```csharp public partial class RegistrationViewModel : ObservableValidator { [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] private string _email = string.Empty; [ObservableProperty] [NotifyDataErrorInfo] [Required] [MinLength(8, ErrorMessage = "Password must be at least 8 characters")] private string _password = string.Empty; [RelayCommand(CanExecute = nameof(CanRegister))] private async Task RegisterAsync() { ValidateAllProperties(); if (HasErrors) return; // Registration logic } private bool CanRegister() => !HasErrors; } ``` ## Dependency Injection ### Registration ```csharp // Services services.AddSingleton(); services.AddSingleton(); // ViewModels services.AddTransient(); services.AddTransient(); // Views (for View-first navigation) services.AddTransient(); services.AddTransient(); ``` ### ViewModel Locator Pattern ```csharp public class ViewModelLocator { private static IServiceProvider _provider = null!; public static void Initialize(IServiceProvider provider) => _provider = provider; public ProductListViewModel ProductList => _provider.GetRequiredService(); public ProductDetailViewModel ProductDetail => _provider.GetRequiredService(); } ``` ## View Binding ### XAML Binding ```xml