Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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,
.claude/skills/managedcode-mvvm/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 137% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 100% | 0% |
| case-11 | ✓→✗ | ▼ Worse | 69% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 65% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 112% | 0% |
See detailed examples in the references/ folder:
patterns.md — ViewModel, command, navigation, and state patternsanti-patterns.md — Common mistakes and how to fix them| Component | Responsibility | Example | |-----------|---------------|---------| | Model | Business logic and data | Product, Order, User | | View | UI presentation (XAML/Razor) | ProductPage.xaml | | ViewModel | UI logic and state | ProductViewModel |
xml<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
csharppublic 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); }
csharppublic 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)); } }
csharppublic partial class ProductListViewModel(IProductService productService) : ObservableObject { [ObservableProperty] private ObservableCollection<ProductViewModel> _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<ProductViewModel>( items.Select(p => new ProductViewModel(productService) { Name = p.Name, Price = p.Price })); } finally { IsLoading = false; } } [RelayCommand] private void DeleteProduct(ProductViewModel product) { Products.Remove(product); } }
csharppublic 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 } }
csharppublic partial class NavigationViewModel : ObservableObject { [RelayCommand] private void NavigateTo(string page) { // Navigate to page } [RelayCommand] private async Task OpenItemAsync(int itemId) { // Load and open item } }
csharp// Define message public record ProductSelectedMessage(Product Product); // Send from one ViewModel WeakReferenceMessenger.Default.Send(new ProductSelectedMessage(selectedProduct));
csharppublic partial class ProductDetailViewModel : ObservableRecipient { public ProductDetailViewModel() { IsActive = true; // Enable message reception } protected override void OnActivated() { Messenger.Register<ProductDetailViewModel, ProductSelectedMessage>( this, (r, m) => r.LoadProduct(m.Product)); } private void LoadProduct(Product product) { // Update UI with product details } }
csharppublic 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; }
csharp// Services services.AddSingleton<IProductService, ProductService>(); services.AddSingleton<INavigationService, NavigationService>(); // ViewModels services.AddTransient<ProductListViewModel>(); services.AddTransient<ProductDetailViewModel>(); // Views (for View-first navigation) services.AddTransient<ProductListPage>(); services.AddTransient<ProductDetailPage>();
csharppublic class ViewModelLocator { private static IServiceProvider _provider = null!; public static void Initialize(IServiceProvider provider) => _provider = provider; public ProductListViewModel ProductList => _provider.GetRequiredService<ProductListViewModel>(); public ProductDetailViewModel ProductDetail => _provider.GetRequiredService<ProductDetailViewModel>(); }
xml<Page x:Class="MyApp.Views.ProductListPage" xmlns:vm="using:MyApp.ViewModels" x:DataType="vm:ProductListViewModel"> <Grid> <ProgressRing IsActive="{x:Bind ViewModel.IsLoading, Mode=OneWay}" Visibility="{x:Bind ViewModel.IsLoading, Mode=OneWay}" /> <ListView ItemsSource="{x:Bind ViewModel.Products, Mode=OneWay}" SelectedItem="{x:Bind ViewModel.SelectedProduct, Mode=TwoWay}"> <ListView.ItemTemplate> <DataTemplate x:DataType="vm:ProductViewModel"> <StackPanel> <TextBlock Text="{x:Bind Name, Mode=OneWay}" /> <TextBlock Text="{x:Bind Price, Mode=OneWay}" /> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> <Button Content="Load" Command="{x:Bind ViewModel.LoadProductsCommand}" /> </Grid> </Page>
| Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Logic in code-behind | Not testable | Move to ViewModel | | ViewModel knows View | Tight coupling | Use interfaces/messaging | | Manual INotifyPropertyChanged | Verbose, error-prone | Use source generators | | God ViewModel | Unmaintainable | Split responsibilities | | Direct service calls in View | Violates separation | Go through ViewModel | | Exposing Model directly | Leaks implementation | Create ViewModel properties |
csharppublic class ProductViewModelTests { [Fact] public async Task LoadProducts_PopulatesCollection() { // Arrange var mockService = new Mock<IProductService>(); mockService.Setup(s => s.GetAllAsync()) .ReturnsAsync([new Product { Name = "Test", Price = 10 }]); var viewModel = new ProductListViewModel(mockService.Object); // Act await viewModel.LoadProductsCommand.ExecuteAsync(null); // Assert Assert.Single(viewModel.Products); Assert.Equal("Test", viewModel.Products[0].Name); } [Fact] public void SaveCommand_CannotExecute_WhenInvalid() { var viewModel = new ProductViewModel(Mock.Of<IProductService>()) { Name = "", IsValid = false }; Assert.False(viewModel.SaveCommand.CanExecute(null)); } }
| Feature | MVVM Toolkit | Prism | MVVMLight | |---------|--------------|-------|-----------| | Source generators | Yes | No | No | | Maintenance | Active | Active | Deprecated | | DI built-in | No | Yes | No | | Navigation | No | Yes | No | | Weight | Light | Heavy | Light |
Other measured skills in the registry, with their headline benchmark lift.