Claude
Skill
dotnet-mvvm
Implement the Model-View-ViewModel pattern in .NET applications with proper separation of concerns, data binding, commands, and testable ViewModels using MVVM Toolkit.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-mvvm-bfa4ebd.zip · 7 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-mvvm
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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
References
See detailed examples in the references/ folder:
patterns.md— ViewModel, command, navigation, and state patternsanti-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
- Keep Views dumb — no business logic in code-behind
- Use data binding — connect View to ViewModel properties
- Commands for actions — handle user interactions via ICommand
- Inject dependencies — services go into ViewModel constructors
- Test ViewModels — they should be unit testable without UI
MVVM Toolkit Setup
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
ViewModel Patterns
Basic ViewModel with Source Generators
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
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
public 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);
}
}
Commands
Async Commands with Cancellation
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
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
// Define message
public record ProductSelectedMessage(Product Product);
// Send from one ViewModel
WeakReferenceMessenger.Default.Send(new ProductSelectedMessage(selectedProduct));
Receiving Messages
public 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
}
}
Validation
Using ObservableValidator
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
// 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>();
ViewModel Locator Pattern
public 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>();
}
View Binding
XAML Binding
<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-Patterns to Avoid
| 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 |
Testing ViewModels
public 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));
}
}
Framework Comparison
| 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 |
Deliver
- ViewModels that are fully unit testable
- Clean separation between UI and business logic
- Proper use of commands and data binding
- Messaging for loose coupling between components
Validate
- No business logic in code-behind files
- ViewModels don't reference View types
- Commands are used for all user actions
- Properties use ObservableProperty or equivalent
- Dependencies are injected, not created
- Unit tests cover ViewModel logic
Files (dotnet-skills)
-
references
-
anti-patterns.md 6.3 KB
# MVVM Anti-Patterns Reference ## 1. Logic in Code-Behind ### Bad ```csharp // MainPage.xaml.cs public partial class MainPage : Page { private async void SaveButton_Click(object sender, RoutedEventArgs e) { var service = new ProductService(); // Direct instantiation await service.SaveAsync(new Product { Name = NameTextBox.Text, Price = decimal.Parse(PriceTextBox.Text) }); MessageBox.Show("Saved!"); } } ``` ### Good ```csharp // MainPage.xaml.cs public partial class MainPage : Page { public MainPage(MainViewModel viewModel) { InitializeComponent(); DataContext = viewModel; } } // MainViewModel.cs public partial class MainViewModel(IProductService productService) : ObservableObject { [RelayCommand] private async Task SaveAsync() { await productService.SaveAsync(new Product { Name = Name, Price = Price }); } } ``` ## 2. ViewModel Referencing View Types ### Bad ```csharp public class MainViewModel : ObservableObject { private readonly MainPage _page; // Direct View reference public void UpdateUI() { _page.StatusLabel.Text = "Updated"; // Manipulating View directly } } ``` ### Good ```csharp public partial class MainViewModel : ObservableObject { [ObservableProperty] private string _statusText = string.Empty; } ``` ## 3. Manual INotifyPropertyChanged ### Bad ```csharp public class ProductViewModel : INotifyPropertyChanged { private string _name; public string Name { get => _name; set { if (_name != value) { _name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } } public event PropertyChangedEventHandler? PropertyChanged; } ``` ### Good ```csharp public partial class ProductViewModel : ObservableObject { [ObservableProperty] private string _name = string.Empty; } ``` ## 4. God ViewModel ### Bad ```csharp public partial class MainViewModel : ObservableObject { // 500+ lines handling products, orders, users, settings, navigation, etc. [ObservableProperty] private ObservableCollection<Product> _products; [ObservableProperty] private ObservableCollection<Order> _orders; [ObservableProperty] private User _currentUser; [ObservableProperty] private AppSettings _settings; // ... hundreds more properties and commands } ``` ### Good ```csharp // Split into focused ViewModels public partial class ProductListViewModel : ObservableObject { /* Products only */ } public partial class OrderListViewModel : ObservableObject { /* Orders only */ } public partial class UserProfileViewModel : ObservableObject { /* User only */ } public partial class SettingsViewModel : ObservableObject { /* Settings only */ } ``` ## 5. Direct Service Calls in View ### Bad ```xml <Button Click="OnClick" /> ``` ```csharp private async void OnClick(object sender, EventArgs e) { var service = App.ServiceProvider.GetService<IDataService>(); var data = await service.GetDataAsync(); // ... } ``` ### Good ```xml <Button Command="{Binding LoadDataCommand}" /> ``` ```csharp public partial class DataViewModel(IDataService service) : ObservableObject { [RelayCommand] private async Task LoadDataAsync() { var data = await service.GetDataAsync(); // ... } } ``` ## 6. Exposing Model Directly ### Bad ```csharp public partial class OrderViewModel : ObservableObject { [ObservableProperty] private Order _order; // Exposes database entity directly // View binds to Order.Customer.Address.City } ``` ### Good ```csharp public partial class OrderViewModel : ObservableObject { private readonly Order _order; public string CustomerName => _order.Customer.Name; public string ShippingAddress => FormatAddress(_order.Customer.Address); public decimal Total => _order.Items.Sum(i => i.Price * i.Quantity); } ``` ## 7. Synchronous Operations on UI Thread ### Bad ```csharp [RelayCommand] private void LoadData() { var data = _service.GetData(); // Blocking call Items = new ObservableCollection<Item>(data); } ``` ### Good ```csharp [RelayCommand] private async Task LoadDataAsync() { var data = await _service.GetDataAsync(); Items = new ObservableCollection<Item>(data); } ``` ## 8. Missing Validation ### Bad ```csharp [RelayCommand] private async Task SaveAsync() { await _service.SaveAsync(new Product { Name = Name, Price = Price }); } ``` ### Good ```csharp public partial class ProductViewModel : ObservableValidator { [ObservableProperty] [NotifyDataErrorInfo] [Required] private string _name = string.Empty; [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { ValidateAllProperties(); if (!HasErrors) { await _service.SaveAsync(new Product { Name = Name, Price = Price }); } } private bool CanSave() => !HasErrors; } ``` ## 9. Tight Coupling Between ViewModels ### Bad ```csharp public class OrderViewModel : ObservableObject { private readonly CustomerViewModel _customerViewModel; // Direct reference public void SelectCustomer() { _customerViewModel.ShowSelectionDialog(); Customer = _customerViewModel.SelectedCustomer; } } ``` ### Good ```csharp public partial class OrderViewModel : ObservableRecipient { public OrderViewModel() { IsActive = true; } protected override void OnActivated() { Messenger.Register<CustomerSelectedMessage>(this, (r, m) => { Customer = m.Customer; }); } } ``` ## 10. Not Using IDisposable ### Bad ```csharp public class LiveDataViewModel : ObservableObject { private readonly Timer _timer; public LiveDataViewModel() { _timer = new Timer(UpdateData, null, 0, 1000); } // Timer never stopped, memory leak } ``` ### Good ```csharp public partial class LiveDataViewModel : ObservableObject, IDisposable { private readonly Timer _timer; private bool _disposed; public LiveDataViewModel() { _timer = new Timer(UpdateData, null, 0, 1000); } public void Dispose() { if (!_disposed) { _timer.Dispose(); _disposed = true; } } } ``` -
patterns.md 6.9 KB
# MVVM Patterns Reference ## Property Patterns ### Simple Observable Property ```csharp [ObservableProperty] private string _name = string.Empty; ``` ### Property with Change Notification ```csharp [ObservableProperty] private int _quantity; partial void OnQuantityChanged(int oldValue, int newValue) { // React to change OnPropertyChanged(nameof(Total)); } ``` ### Property Affecting Commands ```csharp [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] [NotifyCanExecuteChangedFor(nameof(DeleteCommand))] private bool _isValid; ``` ### Property with Validation ```csharp [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] private string _name = string.Empty; ``` ## Command Patterns ### Simple Async Command ```csharp [RelayCommand] private async Task LoadDataAsync() { var data = await _service.GetDataAsync(); Items = new ObservableCollection<Item>(data); } ``` ### Command with CanExecute ```csharp [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { await _service.SaveAsync(CurrentItem); } private bool CanSave() => IsValid && !IsBusy; ``` ### Command with Parameter ```csharp [RelayCommand] private void SelectItem(Item item) { SelectedItem = item; } [RelayCommand] private async Task DeleteItemAsync(int itemId) { await _service.DeleteAsync(itemId); Items.Remove(Items.First(i => i.Id == itemId)); } ``` ### Command with Cancellation ```csharp [RelayCommand(IncludeCancelCommand = true)] private async Task SearchAsync(string query, CancellationToken token) { await Task.Delay(300, token); // Debounce var results = await _searchService.SearchAsync(query, token); SearchResults = new ObservableCollection<SearchResult>(results); } ``` ## Collection Patterns ### Master-Detail ```csharp public partial class MasterDetailViewModel : ObservableObject { [ObservableProperty] private ObservableCollection<Item> _items = []; [ObservableProperty] private Item? _selectedItem; partial void OnSelectedItemChanged(Item? value) { if (value is not null) { LoadDetails(value.Id); } } private async void LoadDetails(int id) { DetailItem = await _service.GetDetailsAsync(id); } } ``` ### Filtered Collection ```csharp public partial class FilteredListViewModel : ObservableObject { private readonly List<Item> _allItems = []; [ObservableProperty] private ObservableCollection<Item> _filteredItems = []; [ObservableProperty] private string _searchText = string.Empty; partial void OnSearchTextChanged(string value) { FilterItems(); } private void FilterItems() { var filtered = string.IsNullOrEmpty(SearchText) ? _allItems : _allItems.Where(i => i.Name.Contains(SearchText, StringComparison.OrdinalIgnoreCase)); FilteredItems = new ObservableCollection<Item>(filtered); } } ``` ## Navigation Patterns ### ViewModel-First Navigation ```csharp public interface INavigationService { Task NavigateToAsync<TViewModel>() where TViewModel : ObservableObject; Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ObservableObject; Task GoBackAsync(); } public partial class MainViewModel(INavigationService navigation) : ObservableObject { [RelayCommand] private async Task OpenDetailsAsync(Item item) { await navigation.NavigateToAsync<ItemDetailViewModel>(item.Id); } } ``` ### View-First Navigation ```csharp // Shell navigation (MAUI) [RelayCommand] private async Task NavigateToSettings() { await Shell.Current.GoToAsync("settings"); } // Frame navigation (WPF/WinUI) [RelayCommand] private void NavigateToSettings() { _frame.Navigate(typeof(SettingsPage)); } ``` ## State Management Patterns ### Loading State ```csharp public partial class DataViewModel : ObservableObject { [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowContent))] [NotifyPropertyChangedFor(nameof(ShowLoading))] [NotifyPropertyChangedFor(nameof(ShowError))] private ViewState _state = ViewState.Idle; [ObservableProperty] private string? _errorMessage; public bool ShowLoading => State == ViewState.Loading; public bool ShowContent => State == ViewState.Success; public bool ShowError => State == ViewState.Error; [RelayCommand] private async Task LoadAsync() { State = ViewState.Loading; try { var data = await _service.GetDataAsync(); Items = new ObservableCollection<Item>(data); State = ViewState.Success; } catch (Exception ex) { ErrorMessage = ex.Message; State = ViewState.Error; } } } public enum ViewState { Idle, Loading, Success, Error } ``` ### Undo/Redo Pattern ```csharp public partial class EditViewModel : ObservableObject { private readonly Stack<Action> _undoStack = new(); private readonly Stack<Action> _redoStack = new(); [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(UndoCommand))] private bool _canUndo; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RedoCommand))] private bool _canRedo; public void ExecuteCommand(Action doAction, Action undoAction) { doAction(); _undoStack.Push(undoAction); _redoStack.Clear(); UpdateCanUndoRedo(); } [RelayCommand(CanExecute = nameof(CanUndo))] private void Undo() { var action = _undoStack.Pop(); action(); _redoStack.Push(action); UpdateCanUndoRedo(); } } ``` ## Dialog Patterns ### Confirmation Dialog ```csharp public interface IDialogService { Task<bool> ConfirmAsync(string title, string message); Task<string?> PromptAsync(string title, string message); Task AlertAsync(string title, string message); } [RelayCommand] private async Task DeleteItemAsync(Item item) { var confirmed = await _dialogService.ConfirmAsync( "Delete Item", $"Are you sure you want to delete '{item.Name}'?"); if (confirmed) { await _service.DeleteAsync(item.Id); Items.Remove(item); } } ``` ## Dependency Injection Patterns ### Constructor Injection (Preferred) ```csharp public partial class ProductViewModel( IProductService productService, INavigationService navigation, IDialogService dialogs) : ObservableObject { // Use injected services directly } ``` ### Factory Pattern for ViewModels ```csharp public interface IViewModelFactory { TViewModel Create<TViewModel>() where TViewModel : ObservableObject; } public class ViewModelFactory(IServiceProvider provider) : IViewModelFactory { public TViewModel Create<TViewModel>() where TViewModel : ObservableObject { return provider.GetRequiredService<TViewModel>(); } } ```
-
-
SKILL.md 10.4 KB
--- name: dotnet-mvvm version: "1.0.0" category: "Cross-Platform UI" description: "Implement the Model-View-ViewModel pattern in .NET applications with proper separation of concerns, data binding, commands, and testable ViewModels using MVVM Toolkit." 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 <PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" /> ``` ## 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<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); } } ``` ## 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<ProductDetailViewModel, ProductSelectedMessage>( 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<IProductService, ProductService>(); services.AddSingleton<INavigationService, NavigationService>(); // ViewModels services.AddTransient<ProductListViewModel>(); services.AddTransient<ProductDetailViewModel>(); // Views (for View-first navigation) services.AddTransient<ProductListPage>(); services.AddTransient<ProductDetailPage>(); ``` ### 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<ProductListViewModel>(); public ProductDetailViewModel ProductDetail => _provider.GetRequiredService<ProductDetailViewModel>(); } ``` ## View Binding ### XAML Binding ```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-Patterns to Avoid | 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 | ## Testing ViewModels ```csharp public 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)); } } ``` ## Framework Comparison | 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 | ## Deliver - ViewModels that are fully unit testable - Clean separation between UI and business logic - Proper use of commands and data binding - Messaging for loose coupling between components ## Validate - No business logic in code-behind files - ViewModels don't reference View types - Commands are used for all user actions - Properties use ObservableProperty or equivalent - Dependencies are injected, not created - Unit tests cover ViewModel logic
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.