Claude
Skill
dotnet-wpf
Build and modernize WPF applications on .NET with correct XAML, data binding, commands, threading, styling, and Windows desktop migration decisions.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-wpf-bfa4ebd.zip · 13 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-wpf
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
WPF
Trigger On
- working on WPF UI, MVVM, binding, commands, or desktop modernization
- migrating WPF from .NET Framework to .NET
- integrating newer Windows capabilities into a WPF app
- implementing data binding, styles, templates, or control customization
Documentation
References
- patterns.md - MVVM patterns, binding patterns, command patterns, and reusable architectural approaches
- anti-patterns.md - Common WPF mistakes and how to avoid them
Workflow
- Confirm Windows-only scope — WPF is Windows-only even when the wider .NET stack is cross-platform
- Apply MVVM pattern — keep views dumb, logic in ViewModels, use commands
- Manage data binding explicitly — choose correct binding modes, validate at runtime
- Use styles and templates deliberately — keep UI composable, avoid page-specific hacks
- Handle threading correctly — use Dispatcher for UI updates, async/await for long operations
- Validate both designer and runtime — XAML composition failures often surface only at runtime
Project Structure
MyWpfApp/
├── MyWpfApp/
│ ├── App.xaml # Application entry
│ ├── MainWindow.xaml # Main window
│ ├── Views/ # XAML views/windows
│ ├── ViewModels/ # MVVM ViewModels
│ ├── Models/ # Domain models
│ ├── Services/ # Business logic
│ ├── Converters/ # Value converters
│ ├── Resources/ # Styles, templates, dictionaries
│ └── Controls/ # Custom controls
└── MyWpfApp.Tests/
MVVM Pattern
ViewModel with MVVM Toolkit
public partial class CustomersViewModel : ObservableObject
{
private readonly ICustomerService _customerService;
[ObservableProperty]
private ObservableCollection<Customer> _customers = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private Customer? _selectedCustomer;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private bool _isLoading;
public CustomersViewModel(ICustomerService customerService)
{
_customerService = customerService;
}
[RelayCommand(CanExecute = nameof(CanRefresh))]
private async Task RefreshAsync()
{
IsLoading = true;
try
{
var items = await _customerService.GetAllAsync();
Customers = new ObservableCollection<Customer>(items);
}
finally
{
IsLoading = false;
}
}
private bool CanRefresh() => !IsLoading;
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
if (SelectedCustomer is null) return;
await _customerService.SaveAsync(SelectedCustomer);
}
private bool CanSave() => SelectedCustomer is not null;
}
View Binding
<Window x:Class="MyWpfApp.Views.CustomersView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MyWpfApp.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:CustomersViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<Button Content="Refresh"
Command="{Binding RefreshCommand}"/>
<Button Content="Save"
Command="{Binding SaveCommand}"/>
</ToolBar>
<DataGrid Grid.Row="1"
ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"/>
<DataGridTextColumn Header="Email"
Binding="{Binding Email}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Dependency Injection
public partial class App : Application
{
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<ICustomerService, CustomerService>();
services.AddSingleton<INavigationService, NavigationService>();
// ViewModels
services.AddTransient<CustomersViewModel>();
services.AddTransient<CustomerDetailViewModel>();
// Views
services.AddTransient<MainWindow>();
services.AddTransient<CustomersView>();
})
.Build();
}
protected override async void OnStartup(StartupEventArgs e)
{
await _host.StartAsync();
var mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
}
}
Data Binding Modes
<!-- OneTime: Read once at initialization -->
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>
<!-- OneWay: Source to target only (default for most properties) -->
<TextBlock Text="{Binding Name, Mode=OneWay}"/>
<!-- TwoWay: Bidirectional synchronization -->
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<!-- OneWayToSource: Target to source only -->
<TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/>
Value Converters
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is Visibility.Visible;
}
}
// Multi-value converter
public class MultiplyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is double a && values[1] is double b)
{
return a * b;
}
return 0.0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Styles and Templates
Resource Dictionary
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Implicit style for all Buttons -->
<Style TargetType="Button">
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Background" Value="#0078D4"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Named style -->
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#D32F2F"/>
</Style>
</ResourceDictionary>
Threading and Dispatcher
// Update UI from background thread
await Task.Run(async () =>
{
var data = await LoadDataAsync();
// Must use Dispatcher to update UI
Application.Current.Dispatcher.Invoke(() =>
{
Items.Clear();
foreach (var item in data)
{
Items.Add(item);
}
});
});
// Better: Use async/await properly
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
// This runs on background thread
var data = await _service.GetDataAsync();
// This automatically marshals to UI thread
Items = new ObservableCollection<Item>(data);
}
finally
{
IsLoading = false;
}
}
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Logic in code-behind | Hard to test, tight coupling | Use MVVM with ViewModels |
| Synchronous blocking calls | UI freezes | Use async/await |
| Manual INotifyPropertyChanged | Boilerplate, error-prone | Use MVVM Toolkit attributes |
| Hardcoded colors/sizes | Inconsistent, hard to theme | Use resource dictionaries |
| Direct Dispatcher.Invoke everywhere | Complex, error-prone | Prefer async/await marshaling |
| God ViewModel | Unmaintainable | Split into focused ViewModels |
| Skipping binding validation | Runtime errors hidden | Use ValidatesOnDataErrors |
| Event handlers for everything | Memory leaks, coupling | Use commands and bindings |
Best Practices
Use compiled bindings in .NET 5+:
- Enable
x:CompileBindings="True"for performance
- Enable
Implement INotifyDataErrorInfo for validation:
[ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] private string _name = string.Empty;Use weak event patterns for long-lived subscriptions:
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler);Virtualize large collections:
<ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/>Freeze Freezables when possible:
var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // Thread-safe, better performanceUse design-time data:
<Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
Testing
[Fact]
public async Task RefreshCommand_LoadsCustomers()
{
var mockService = new Mock<ICustomerService>();
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(new[] { new Customer { Name = "Test" } });
var viewModel = new CustomersViewModel(mockService.Object);
await viewModel.RefreshCommand.ExecuteAsync(null);
Assert.Single(viewModel.Customers);
Assert.Equal("Test", viewModel.Customers[0].Name);
}
[Fact]
public void SaveCommand_CannotExecute_WhenNoSelection()
{
var mockService = new Mock<ICustomerService>();
var viewModel = new CustomersViewModel(mockService.Object);
viewModel.SelectedCustomer = null;
Assert.False(viewModel.SaveCommand.CanExecute(null));
}
Deliver
- cleaner WPF views and view-model boundaries
- safer binding and threading behavior
- migration guidance grounded in actual Windows constraints
- MVVM pattern with testable ViewModels
Validate
- binding and command flows are explicit
- code-behind is not carrying hidden business logic
- Windows-only assumptions are acknowledged
- threading and dispatcher usage is correct
- styles and resources are properly organized
Files (dotnet-skills)
-
references
-
anti-patterns.md 18 KB
# WPF Anti-Patterns Reference ## MVVM Violations ### Logic in Code-Behind **Problem**: Business logic in XAML code-behind files makes testing difficult and creates tight coupling. ```csharp // WRONG: Logic in code-behind public partial class CustomerWindow : Window { private readonly CustomerService _service = new(); public CustomerWindow() { InitializeComponent(); } private async void SaveButton_Click(object sender, RoutedEventArgs e) { // Business logic should not be here if (string.IsNullOrEmpty(NameTextBox.Text)) { MessageBox.Show("Name is required"); return; } var customer = new Customer { Name = NameTextBox.Text }; await _service.SaveAsync(customer); CustomerList.Items.Add(customer); } } ``` **Solution**: Move logic to ViewModel with commands. ```csharp // CORRECT: Logic in ViewModel public partial class CustomerViewModel : ObservableObject { private readonly ICustomerService _service; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private string _name = string.Empty; [ObservableProperty] private ObservableCollection<Customer> _customers = []; [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { var customer = new Customer { Name = Name }; await _service.SaveAsync(customer); Customers.Add(customer); Name = string.Empty; } private bool CanSave() => !string.IsNullOrEmpty(Name); } ``` ### God ViewModel **Problem**: Single ViewModel handling too many responsibilities becomes unmaintainable. ```csharp // WRONG: ViewModel doing everything public partial class MainViewModel : ObservableObject { // Customer management [ObservableProperty] private ObservableCollection<Customer> _customers; [ObservableProperty] private Customer? _selectedCustomer; // Order management [ObservableProperty] private ObservableCollection<Order> _orders; [ObservableProperty] private Order? _selectedOrder; // Product management [ObservableProperty] private ObservableCollection<Product> _products; [ObservableProperty] private Product? _selectedProduct; // Settings [ObservableProperty] private AppSettings _settings; // ... hundreds more properties and commands } ``` **Solution**: Split into focused ViewModels with composition. ```csharp // CORRECT: Focused ViewModels public partial class ShellViewModel : ObservableObject { public CustomersViewModel Customers { get; } public OrdersViewModel Orders { get; } public ProductsViewModel Products { get; } public SettingsViewModel Settings { get; } public ShellViewModel( CustomersViewModel customers, OrdersViewModel orders, ProductsViewModel products, SettingsViewModel settings) { Customers = customers; Orders = orders; Products = products; Settings = settings; } } ``` ### ViewModel with View Dependencies **Problem**: ViewModel directly references UI elements, breaking testability. ```csharp // WRONG: ViewModel knows about View public class BadViewModel { private readonly Window _window; private readonly TextBox _nameTextBox; public BadViewModel(Window window, TextBox nameTextBox) { _window = window; _nameTextBox = nameTextBox; } public void Save() { var name = _nameTextBox.Text; // Direct UI access _window.Close(); // Controlling window from ViewModel } } ``` **Solution**: Use services and messaging for UI interactions. ```csharp // CORRECT: ViewModel uses abstractions public partial class GoodViewModel : ObservableObject { private readonly IDialogService _dialogService; private readonly INavigationService _navigationService; [ObservableProperty] private string _name = string.Empty; public GoodViewModel(IDialogService dialogService, INavigationService navigationService) { _dialogService = dialogService; _navigationService = navigationService; } [RelayCommand] private async Task SaveAsync() { await _service.SaveAsync(Name); _navigationService.GoBack(); } } ``` ## Data Binding Mistakes ### Missing INotifyPropertyChanged **Problem**: Properties that don't notify changes won't update the UI. ```csharp // WRONG: No change notification public class Person { public string Name { get; set; } = string.Empty; // UI won't update } ``` **Solution**: Use ObservableObject or implement INotifyPropertyChanged. ```csharp // CORRECT: With MVVM Toolkit public partial class Person : ObservableObject { [ObservableProperty] private string _name = string.Empty; } ``` ### Replacing Observable Collections Incorrectly **Problem**: Replacing items in ObservableCollection doesn't notify properly in some scenarios. ```csharp // WRONG: May not trigger proper updates Items[0] = newItem; // WRONG: Clearing and re-adding loses selection state Items.Clear(); foreach (var item in newItems) { Items.Add(item); } ``` **Solution**: Use appropriate collection manipulation. ```csharp // CORRECT: Replace entire collection when doing bulk updates Items = new ObservableCollection<Item>(newItems); // CORRECT: For in-place updates, use RemoveAt/Insert var index = Items.IndexOf(oldItem); Items.RemoveAt(index); Items.Insert(index, newItem); ``` ### Wrong Binding Mode **Problem**: Using incorrect binding mode leads to unexpected behavior. ```xml <!-- WRONG: TextBox with OneWay binding won't update source --> <TextBox Text="{Binding Name, Mode=OneWay}"/> <!-- WRONG: Label with TwoWay binding is wasteful --> <Label Content="{Binding Status, Mode=TwoWay}"/> ``` **Solution**: Use appropriate binding modes. ```xml <!-- CORRECT: TextBox with TwoWay for editing --> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <!-- CORRECT: Label with OneWay for display --> <Label Content="{Binding Status, Mode=OneWay}"/> <!-- CORRECT: OneTime for static content --> <TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/> ``` ### Binding Errors Ignored **Problem**: Silent binding failures in output window go unnoticed. ```xml <!-- WRONG: Typo in binding path fails silently --> <TextBlock Text="{Binding Nmae}"/> <!-- Should be "Name" --> ``` **Solution**: Enable binding diagnostics and validate at design time. ```xml <!-- Enable detailed binding errors in App.xaml --> <Application xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"> <!-- In resources or startup code --> </Application> ``` ```csharp // In App.xaml.cs #if DEBUG PresentationTraceSources.DataBindingSource.Listeners.Add(new ConsoleTraceListener()); PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Warning; #endif ``` ## Threading Mistakes ### Blocking UI Thread **Problem**: Synchronous operations freeze the UI. ```csharp // WRONG: Blocks UI thread private void LoadButton_Click(object sender, RoutedEventArgs e) { var data = _service.LoadData(); // Synchronous call DataGrid.ItemsSource = data; } ``` **Solution**: Use async/await. ```csharp // CORRECT: Async operation [RelayCommand] private async Task LoadAsync() { var data = await _service.LoadDataAsync(); Items = new ObservableCollection<Item>(data); } ``` ### Modifying UI from Background Thread **Problem**: Updating UI elements from non-UI thread causes exceptions. ```csharp // WRONG: Cross-thread violation await Task.Run(() => { var data = LoadHeavyData(); Items.Add(data); // Crash: collection modified from wrong thread }); ``` **Solution**: Marshal UI updates to dispatcher. ```csharp // CORRECT: Use Dispatcher or proper async pattern await Task.Run(async () => { var data = LoadHeavyData(); await Application.Current.Dispatcher.InvokeAsync(() => { Items.Add(data); }); }); // BETTER: Let async/await handle marshaling var data = await Task.Run(() => LoadHeavyData()); Items.Add(data); // Back on UI thread automatically ``` ### Nested Dispatcher.Invoke Calls **Problem**: Excessive Dispatcher.Invoke calls hurt performance. ```csharp // WRONG: Multiple dispatcher calls foreach (var item in items) { Application.Current.Dispatcher.Invoke(() => { Items.Add(item); // Called for each item }); } ``` **Solution**: Batch UI updates. ```csharp // CORRECT: Single dispatcher call Application.Current.Dispatcher.Invoke(() => { foreach (var item in items) { Items.Add(item); } }); // BEST: Replace entire collection var newItems = await Task.Run(() => ProcessItems(items)); Items = new ObservableCollection<Item>(newItems); ``` ## Memory Leaks ### Event Handler Memory Leaks **Problem**: Not unsubscribing from events prevents garbage collection. ```csharp // WRONG: Memory leak public class ChildViewModel { public ChildViewModel(ParentService service) { service.DataChanged += OnDataChanged; // Never unsubscribed } private void OnDataChanged(object? sender, EventArgs e) { // Handle event } } ``` **Solution**: Use weak events or implement IDisposable. ```csharp // CORRECT: Weak event pattern public class ChildViewModel { public ChildViewModel(ParentService service) { WeakEventManager<ParentService, EventArgs>.AddHandler( service, nameof(ParentService.DataChanged), OnDataChanged); } } // CORRECT: IDisposable pattern public class ChildViewModel : IDisposable { private readonly ParentService _service; public ChildViewModel(ParentService service) { _service = service; _service.DataChanged += OnDataChanged; } public void Dispose() { _service.DataChanged -= OnDataChanged; } } ``` ### Binding Memory Leaks **Problem**: Binding to non-INotifyPropertyChanged objects can cause leaks. ```csharp // WRONG: Binding to plain object public class Person { public string Name { get; set; } // No INPC } // View binds to this, WPF creates strong reference ``` **Solution**: Always implement INotifyPropertyChanged for bound objects. ```csharp // CORRECT: Observable object public partial class Person : ObservableObject { [ObservableProperty] private string _name = string.Empty; } ``` ### Static Event Handlers **Problem**: Static events hold references to subscribers forever. ```csharp // WRONG: Static event public static class EventAggregator { public static event EventHandler? SomethingHappened; } // Subscriber never gets collected EventAggregator.SomethingHappened += OnSomethingHappened; ``` **Solution**: Use weak reference messaging. ```csharp // CORRECT: Weak reference messenger WeakReferenceMessenger.Default.Register<SomeMessage>(this, (r, m) => { // Handle message }); // Unregister when done WeakReferenceMessenger.Default.Unregister<SomeMessage>(this); ``` ## Style and Template Mistakes ### Hardcoded Values **Problem**: Hardcoded colors, sizes, and fonts prevent theming. ```xml <!-- WRONG: Hardcoded values --> <Button Background="#0078D4" FontSize="14" Padding="10,5" Foreground="White"/> ``` **Solution**: Use resource dictionaries. ```xml <!-- CORRECT: Resources --> <Button Style="{StaticResource PrimaryButton}"/> <!-- In ResourceDictionary --> <SolidColorBrush x:Key="PrimaryBrush" Color="#0078D4"/> <sys:Double x:Key="StandardFontSize">14</sys:Double> <Style x:Key="PrimaryButton" TargetType="Button"> <Setter Property="Background" Value="{StaticResource PrimaryBrush}"/> <Setter Property="FontSize" Value="{StaticResource StandardFontSize}"/> <Setter Property="Foreground" Value="White"/> <Setter Property="Padding" Value="10,5"/> </Style> ``` ### Inline Styles Everywhere **Problem**: Repeated inline styles are hard to maintain. ```xml <!-- WRONG: Repeated inline styling --> <StackPanel> <TextBlock FontSize="24" FontWeight="Bold" Margin="0,0,0,10"/> <TextBlock FontSize="24" FontWeight="Bold" Margin="0,0,0,10"/> <TextBlock FontSize="24" FontWeight="Bold" Margin="0,0,0,10"/> </StackPanel> ``` **Solution**: Define reusable styles. ```xml <!-- CORRECT: Reusable style --> <StackPanel> <StackPanel.Resources> <Style x:Key="HeaderText" TargetType="TextBlock"> <Setter Property="FontSize" Value="24"/> <Setter Property="FontWeight" Value="Bold"/> <Setter Property="Margin" Value="0,0,0,10"/> </Style> </StackPanel.Resources> <TextBlock Style="{StaticResource HeaderText}"/> <TextBlock Style="{StaticResource HeaderText}"/> <TextBlock Style="{StaticResource HeaderText}"/> </StackPanel> ``` ### Forgetting BasedOn for Derived Styles **Problem**: Derived styles lose base styling. ```xml <!-- WRONG: Loses default Button styling --> <Style x:Key="RedButton" TargetType="Button"> <Setter Property="Background" Value="Red"/> </Style> ``` **Solution**: Use BasedOn. ```xml <!-- CORRECT: Inherits base style --> <Style x:Key="RedButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}"> <Setter Property="Background" Value="Red"/> </Style> ``` ## Performance Anti-Patterns ### Not Virtualizing Large Lists **Problem**: Rendering thousands of items without virtualization. ```xml <!-- WRONG: No virtualization --> <ItemsControl ItemsSource="{Binding ThousandsOfItems}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel/> <!-- StackPanel doesn't virtualize --> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> ``` **Solution**: Enable virtualization. ```xml <!-- CORRECT: Virtualized list --> <ListBox ItemsSource="{Binding ThousandsOfItems}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.ScrollUnit="Pixel"/> ``` ### Creating Brushes in Property Getters **Problem**: Creating new objects in property getters causes allocations every access. ```csharp // WRONG: New brush created every access public Brush StatusBrush => IsActive ? new SolidColorBrush(Colors.Green) : new SolidColorBrush(Colors.Red); ``` **Solution**: Cache brushes or use static resources. ```csharp // CORRECT: Static cached brushes private static readonly Brush ActiveBrush = CreateFrozenBrush(Colors.Green); private static readonly Brush InactiveBrush = CreateFrozenBrush(Colors.Red); private static SolidColorBrush CreateFrozenBrush(Color color) { var brush = new SolidColorBrush(color); brush.Freeze(); return brush; } public Brush StatusBrush => IsActive ? ActiveBrush : InactiveBrush; ``` ### Complex Value Converters **Problem**: Heavy computation in converters runs on every binding update. ```csharp // WRONG: Expensive converter public class ExpensiveConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Heavy computation that runs frequently return ComputeHeavyTransformation(value); } } ``` **Solution**: Move computation to ViewModel or cache results. ```csharp // CORRECT: Computation in ViewModel with caching public partial class ItemViewModel : ObservableObject { [ObservableProperty] private string _rawData = string.Empty; [ObservableProperty] private string _processedData = string.Empty; partial void OnRawDataChanged(string value) { ProcessedData = ComputeTransformation(value); // Computed once per change } } ``` ## Command Mistakes ### Async Void Commands **Problem**: Async void swallows exceptions and can't be awaited. ```csharp // WRONG: async void private async void SaveCommand_Execute() { await _service.SaveAsync(); // Exception here is lost } ``` **Solution**: Use proper async command patterns. ```csharp // CORRECT: MVVM Toolkit async command [RelayCommand] private async Task SaveAsync() { await _service.SaveAsync(); } ``` ### Not Calling CanExecuteChanged **Problem**: Command button stays enabled/disabled incorrectly. ```csharp // WRONG: Manual command without CanExecute updates public class ManualCommand : ICommand { private bool _canExecute = true; public bool CanExecute(object? parameter) => _canExecute; public void SetCanExecute(bool value) { _canExecute = value; // Forgot to call CanExecuteChanged! } } ``` **Solution**: Use MVVM Toolkit attributes or raise CanExecuteChanged. ```csharp // CORRECT: Automatic CanExecute notification [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private bool _hasChanges; [RelayCommand(CanExecute = nameof(CanSave))] private Task SaveAsync() => _service.SaveAsync(); private bool CanSave() => HasChanges; ``` ## Validation Mistakes ### Validation Only in UI **Problem**: Validation rules only in XAML, not enforced in ViewModel. ```xml <!-- WRONG: Only UI validation --> <TextBox> <TextBox.Text> <Binding Path="Email"> <Binding.ValidationRules> <local:EmailValidationRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ``` **Solution**: Implement INotifyDataErrorInfo in ViewModel. ```csharp // CORRECT: ViewModel validation public partial class UserViewModel : ObservableValidator { [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] private string _email = string.Empty; [RelayCommand] private async Task SaveAsync() { ValidateAllProperties(); if (HasErrors) return; await _service.SaveAsync(); } } ``` ### Ignoring Validation Errors **Problem**: Not checking HasErrors before saving. ```csharp // WRONG: Save without validation check [RelayCommand] private async Task SaveAsync() { await _service.SaveAsync(CurrentItem); // May save invalid data } ``` **Solution**: Always validate before save. ```csharp // CORRECT: Validate first [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { ValidateAllProperties(); if (HasErrors) return; await _service.SaveAsync(CurrentItem); } private bool CanSave() => !HasErrors && CurrentItem is not null; ``` -
patterns.md 17.4 KB
# WPF Patterns Reference ## MVVM Patterns ### Basic MVVM Structure The Model-View-ViewModel pattern separates concerns: - **Model**: Business logic and data - **View**: XAML UI, no business logic - **ViewModel**: Presentation logic, exposes data and commands to the View ``` View (XAML) ←→ ViewModel (C#) ←→ Model (C#) ↑ ↑ DataBinding Services/Repositories ``` ### ViewModel Base Class with MVVM Toolkit ```csharp public partial class BaseViewModel : ObservableObject { [ObservableProperty] private bool _isBusy; [ObservableProperty] private string _title = string.Empty; protected async Task ExecuteBusyActionAsync(Func<Task> action) { if (IsBusy) return; try { IsBusy = true; await action(); } finally { IsBusy = false; } } } ``` ### Navigation ViewModel Pattern ```csharp public interface INavigationService { void NavigateTo<TViewModel>() where TViewModel : ObservableObject; void NavigateTo<TViewModel>(object parameter) where TViewModel : ObservableObject; void GoBack(); bool CanGoBack { get; } } public partial class ShellViewModel : ObservableObject { private readonly INavigationService _navigation; [ObservableProperty] private ObservableObject? _currentViewModel; public ShellViewModel(INavigationService navigation) { _navigation = navigation; } [RelayCommand] private void NavigateToCustomers() { _navigation.NavigateTo<CustomersViewModel>(); } [RelayCommand] private void NavigateToSettings() { _navigation.NavigateTo<SettingsViewModel>(); } } ``` ### Messenger Pattern for Decoupled Communication ```csharp // Message definition public sealed record CustomerSelectedMessage(Customer Customer); // Sender public partial class CustomerListViewModel : ObservableObject { [RelayCommand] private void SelectCustomer(Customer customer) { WeakReferenceMessenger.Default.Send(new CustomerSelectedMessage(customer)); } } // Receiver public partial class CustomerDetailViewModel : ObservableObject, IRecipient<CustomerSelectedMessage> { public CustomerDetailViewModel() { WeakReferenceMessenger.Default.Register(this); } public void Receive(CustomerSelectedMessage message) { CurrentCustomer = message.Customer; } [ObservableProperty] private Customer? _currentCustomer; } ``` ### Dialog Service Pattern ```csharp public interface IDialogService { Task<bool> ShowConfirmationAsync(string title, string message); Task ShowErrorAsync(string title, string message); Task<string?> ShowInputAsync(string title, string prompt); Task<T?> ShowDialogAsync<T>(object viewModel) where T : class; } public class DialogService : IDialogService { public async Task<bool> ShowConfirmationAsync(string title, string message) { var result = MessageBox.Show( message, title, MessageBoxButton.YesNo, MessageBoxImage.Question); return await Task.FromResult(result == MessageBoxResult.Yes); } public async Task ShowErrorAsync(string title, string message) { MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Error); await Task.CompletedTask; } public async Task<string?> ShowInputAsync(string title, string prompt) { var dialog = new InputDialog(title, prompt); return dialog.ShowDialog() == true ? await Task.FromResult(dialog.InputText) : null; } public async Task<T?> ShowDialogAsync<T>(object viewModel) where T : class { // Implementation for custom dialogs with ViewModels throw new NotImplementedException(); } } ``` ## Binding Patterns ### Property Change Notification ```csharp // Using MVVM Toolkit source generators public partial class PersonViewModel : ObservableObject { [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] private string _firstName = string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(FullName))] private string _lastName = string.Empty; public string FullName => $"{FirstName} {LastName}"; } ``` ### Collection Binding with Filtering ```csharp public partial class FilterableListViewModel : ObservableObject { private readonly ObservableCollection<Item> _allItems = []; [ObservableProperty] private string _filterText = string.Empty; public ICollectionView ItemsView { get; } public FilterableListViewModel() { ItemsView = CollectionViewSource.GetDefaultView(_allItems); ItemsView.Filter = FilterItems; } partial void OnFilterTextChanged(string value) { ItemsView.Refresh(); } private bool FilterItems(object obj) { if (string.IsNullOrWhiteSpace(FilterText)) return true; if (obj is Item item) { return item.Name.Contains(FilterText, StringComparison.OrdinalIgnoreCase); } return false; } } ``` ### Master-Detail Binding ```xml <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="300"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!-- Master List --> <ListBox Grid.Column="0" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" DisplayMemberPath="Name"/> <!-- Detail View --> <ContentControl Grid.Column="1" Content="{Binding SelectedItem}"> <ContentControl.ContentTemplate> <DataTemplate> <StackPanel Margin="10"> <TextBlock Text="{Binding Name}" FontSize="24"/> <TextBlock Text="{Binding Description}" TextWrapping="Wrap"/> </StackPanel> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl> </Grid> ``` ### Binding to Nested Properties ```xml <!-- Direct nested binding --> <TextBlock Text="{Binding Customer.Address.City}"/> <!-- With fallback for null --> <TextBlock Text="{Binding Customer.Address.City, FallbackValue='N/A', TargetNullValue='Not set'}"/> <!-- With string format --> <TextBlock Text="{Binding Order.Total, StringFormat='{}{0:C}'}"/> <!-- With multi-binding --> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat="{}{0}, {1}"> <Binding Path="Customer.LastName"/> <Binding Path="Customer.FirstName"/> </MultiBinding> </TextBlock.Text> </TextBlock> ``` ### DataTemplate Selector Pattern ```csharp public class MessageTemplateSelector : DataTemplateSelector { public DataTemplate? TextMessageTemplate { get; set; } public DataTemplate? ImageMessageTemplate { get; set; } public DataTemplate? SystemMessageTemplate { get; set; } public override DataTemplate? SelectTemplate(object item, DependencyObject container) { return item switch { TextMessage => TextMessageTemplate, ImageMessage => ImageMessageTemplate, SystemMessage => SystemMessageTemplate, _ => base.SelectTemplate(item, container) }; } } ``` ```xml <Window.Resources> <DataTemplate x:Key="TextMessageTemplate"> <TextBlock Text="{Binding Content}"/> </DataTemplate> <DataTemplate x:Key="ImageMessageTemplate"> <Image Source="{Binding ImageUrl}"/> </DataTemplate> <DataTemplate x:Key="SystemMessageTemplate"> <TextBlock Text="{Binding Content}" FontStyle="Italic"/> </DataTemplate> <local:MessageTemplateSelector x:Key="MessageSelector" TextMessageTemplate="{StaticResource TextMessageTemplate}" ImageMessageTemplate="{StaticResource ImageMessageTemplate}" SystemMessageTemplate="{StaticResource SystemMessageTemplate}"/> </Window.Resources> <ItemsControl ItemsSource="{Binding Messages}" ItemTemplateSelector="{StaticResource MessageSelector}"/> ``` ## Command Patterns ### Async Commands with MVVM Toolkit ```csharp public partial class DataViewModel : ObservableObject { private readonly IDataService _dataService; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoadCommand))] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private bool _isLoading; [ObservableProperty] private Data? _data; public DataViewModel(IDataService dataService) { _dataService = dataService; } [RelayCommand(CanExecute = nameof(CanLoad))] private async Task LoadAsync(CancellationToken token) { IsLoading = true; try { Data = await _dataService.LoadAsync(token); } finally { IsLoading = false; } } private bool CanLoad() => !IsLoading; [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { if (Data is null) return; IsLoading = true; try { await _dataService.SaveAsync(Data); } finally { IsLoading = false; } } private bool CanSave() => !IsLoading && Data is not null; } ``` ### Parameterized Commands ```csharp public partial class ItemsViewModel : ObservableObject { [ObservableProperty] private ObservableCollection<Item> _items = []; [RelayCommand] private void DeleteItem(Item item) { Items.Remove(item); } [RelayCommand] private async Task EditItemAsync(Item item) { var editedItem = await _dialogService.ShowEditDialogAsync(item); if (editedItem is not null) { var index = Items.IndexOf(item); Items[index] = editedItem; } } } ``` ```xml <ListBox ItemsSource="{Binding Items}"> <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}"/> <Button Content="Edit" Command="{Binding DataContext.EditItemCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}"/> <Button Content="Delete" Command="{Binding DataContext.DeleteItemCommand, RelativeSource={RelativeSource AncestorType=ListBox}}" CommandParameter="{Binding}"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> ``` ### Composite Commands ```csharp public interface ICompositeCommandManager { ICommand SaveAllCommand { get; } void RegisterSaveCommand(ICommand command); void UnregisterSaveCommand(ICommand command); } public class CompositeCommandManager : ICompositeCommandManager { private readonly List<ICommand> _saveCommands = []; private readonly RelayCommand _saveAllCommand; public ICommand SaveAllCommand => _saveAllCommand; public CompositeCommandManager() { _saveAllCommand = new RelayCommand( () => ExecuteAll(_saveCommands), () => _saveCommands.All(c => c.CanExecute(null))); } public void RegisterSaveCommand(ICommand command) { _saveCommands.Add(command); command.CanExecuteChanged += OnCommandCanExecuteChanged; } public void UnregisterSaveCommand(ICommand command) { _saveCommands.Remove(command); command.CanExecuteChanged -= OnCommandCanExecuteChanged; } private void OnCommandCanExecuteChanged(object? sender, EventArgs e) { _saveAllCommand.NotifyCanExecuteChanged(); } private static void ExecuteAll(IEnumerable<ICommand> commands) { foreach (var command in commands.Where(c => c.CanExecute(null))) { command.Execute(null); } } } ``` ## View Locator Pattern ```csharp public interface IViewLocator { FrameworkElement? ResolveView(object viewModel); } public class ViewLocator : IViewLocator { private readonly IServiceProvider _serviceProvider; private readonly Dictionary<Type, Type> _viewModelToViewMap = new(); public ViewLocator(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; RegisterMappings(); } private void RegisterMappings() { _viewModelToViewMap[typeof(CustomersViewModel)] = typeof(CustomersView); _viewModelToViewMap[typeof(OrdersViewModel)] = typeof(OrdersView); _viewModelToViewMap[typeof(SettingsViewModel)] = typeof(SettingsView); } public FrameworkElement? ResolveView(object viewModel) { var viewModelType = viewModel.GetType(); if (_viewModelToViewMap.TryGetValue(viewModelType, out var viewType)) { var view = (FrameworkElement)_serviceProvider.GetRequiredService(viewType); view.DataContext = viewModel; return view; } return null; } } ``` ## Attached Behavior Pattern ```csharp public static class TextBoxBehaviors { public static readonly DependencyProperty SelectAllOnFocusProperty = DependencyProperty.RegisterAttached( "SelectAllOnFocus", typeof(bool), typeof(TextBoxBehaviors), new PropertyMetadata(false, OnSelectAllOnFocusChanged)); public static bool GetSelectAllOnFocus(DependencyObject obj) => (bool)obj.GetValue(SelectAllOnFocusProperty); public static void SetSelectAllOnFocus(DependencyObject obj, bool value) => obj.SetValue(SelectAllOnFocusProperty, value); private static void OnSelectAllOnFocusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TextBox textBox) { if ((bool)e.NewValue) { textBox.GotFocus += TextBox_GotFocus; } else { textBox.GotFocus -= TextBox_GotFocus; } } } private static void TextBox_GotFocus(object sender, RoutedEventArgs e) { if (sender is TextBox textBox) { textBox.SelectAll(); } } } ``` ```xml <TextBox local:TextBoxBehaviors.SelectAllOnFocus="True"/> ``` ## Validation Pattern with INotifyDataErrorInfo ```csharp public partial class CustomerFormViewModel : ObservableValidator { [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] [MaxLength(100, ErrorMessage = "Name cannot exceed 100 characters")] private string _name = string.Empty; [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] private string _email = string.Empty; [ObservableProperty] [NotifyDataErrorInfo] [Range(0, 150, ErrorMessage = "Age must be between 0 and 150")] private int _age; [RelayCommand(CanExecute = nameof(CanSubmit))] private async Task SubmitAsync() { ValidateAllProperties(); if (HasErrors) return; await _customerService.SaveAsync(new Customer { Name = Name, Email = Email, Age = Age }); } private bool CanSubmit() => !HasErrors; } ``` ```xml <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"/> <ItemsControl ItemsSource="{Binding (Validation.Errors), RelativeSource={RelativeSource Self}}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding ErrorContent}" Foreground="Red"/> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> ``` ## State Management Pattern ```csharp public enum ViewState { Loading, Loaded, Empty, Error } public partial class StatefulViewModel : ObservableObject { [ObservableProperty] private ViewState _state = ViewState.Loading; [ObservableProperty] private string? _errorMessage; [ObservableProperty] private ObservableCollection<Item> _items = []; [RelayCommand] private async Task LoadAsync() { State = ViewState.Loading; ErrorMessage = null; try { var data = await _service.GetItemsAsync(); Items = new ObservableCollection<Item>(data); State = Items.Count > 0 ? ViewState.Loaded : ViewState.Empty; } catch (Exception ex) { ErrorMessage = ex.Message; State = ViewState.Error; } } } ``` ```xml <Grid> <!-- Loading State --> <ProgressBar IsIndeterminate="True" Visibility="{Binding State, Converter={StaticResource StateToVisibility}, ConverterParameter=Loading}"/> <!-- Loaded State --> <ListBox ItemsSource="{Binding Items}" Visibility="{Binding State, Converter={StaticResource StateToVisibility}, ConverterParameter=Loaded}"/> <!-- Empty State --> <TextBlock Text="No items found" Visibility="{Binding State, Converter={StaticResource StateToVisibility}, ConverterParameter=Empty}"/> <!-- Error State --> <StackPanel Visibility="{Binding State, Converter={StaticResource StateToVisibility}, ConverterParameter=Error}"> <TextBlock Text="{Binding ErrorMessage}" Foreground="Red"/> <Button Content="Retry" Command="{Binding LoadCommand}"/> </StackPanel> </Grid> ```
-
-
SKILL.md 12.5 KB
--- name: dotnet-wpf version: "1.0.0" category: "Desktop" description: "Build and modernize WPF applications on .NET with correct XAML, data binding, commands, threading, styling, and Windows desktop migration decisions." compatibility: "Requires a WPF project on .NET or .NET Framework." --- # WPF ## Trigger On - working on WPF UI, MVVM, binding, commands, or desktop modernization - migrating WPF from .NET Framework to .NET - integrating newer Windows capabilities into a WPF app - implementing data binding, styles, templates, or control customization ## Documentation - [WPF Overview](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/overview/) - [Data Binding Overview](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/) - [MVVM Toolkit Introduction](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) - [Styles and Templates](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/controls/styles-templates-overview) - [Migration Guide](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/migration/) ### References - [patterns.md](references/patterns.md) - MVVM patterns, binding patterns, command patterns, and reusable architectural approaches - [anti-patterns.md](references/anti-patterns.md) - Common WPF mistakes and how to avoid them ## Workflow 1. **Confirm Windows-only scope** — WPF is Windows-only even when the wider .NET stack is cross-platform 2. **Apply MVVM pattern** — keep views dumb, logic in ViewModels, use commands 3. **Manage data binding explicitly** — choose correct binding modes, validate at runtime 4. **Use styles and templates deliberately** — keep UI composable, avoid page-specific hacks 5. **Handle threading correctly** — use Dispatcher for UI updates, async/await for long operations 6. **Validate both designer and runtime** — XAML composition failures often surface only at runtime ## Project Structure ``` MyWpfApp/ ├── MyWpfApp/ │ ├── App.xaml # Application entry │ ├── MainWindow.xaml # Main window │ ├── Views/ # XAML views/windows │ ├── ViewModels/ # MVVM ViewModels │ ├── Models/ # Domain models │ ├── Services/ # Business logic │ ├── Converters/ # Value converters │ ├── Resources/ # Styles, templates, dictionaries │ └── Controls/ # Custom controls └── MyWpfApp.Tests/ ``` ## MVVM Pattern ### ViewModel with MVVM Toolkit ```csharp public partial class CustomersViewModel : ObservableObject { private readonly ICustomerService _customerService; [ObservableProperty] private ObservableCollection<Customer> _customers = []; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(SaveCommand))] private Customer? _selectedCustomer; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RefreshCommand))] private bool _isLoading; public CustomersViewModel(ICustomerService customerService) { _customerService = customerService; } [RelayCommand(CanExecute = nameof(CanRefresh))] private async Task RefreshAsync() { IsLoading = true; try { var items = await _customerService.GetAllAsync(); Customers = new ObservableCollection<Customer>(items); } finally { IsLoading = false; } } private bool CanRefresh() => !IsLoading; [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { if (SelectedCustomer is null) return; await _customerService.SaveAsync(SelectedCustomer); } private bool CanSave() => SelectedCustomer is not null; } ``` ### View Binding ```xml <Window x:Class="MyWpfApp.Views.CustomersView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:MyWpfApp.ViewModels" d:DataContext="{d:DesignInstance Type=vm:CustomersViewModel}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <ToolBar Grid.Row="0"> <Button Content="Refresh" Command="{Binding RefreshCommand}"/> <Button Content="Save" Command="{Binding SaveCommand}"/> </ToolBar> <DataGrid Grid.Row="1" ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}"/> <DataGridTextColumn Header="Email" Binding="{Binding Email}"/> </DataGrid.Columns> </DataGrid> </Grid> </Window> ``` ## Dependency Injection ```csharp public partial class App : Application { private readonly IHost _host; public App() { _host = Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { // Services services.AddSingleton<ICustomerService, CustomerService>(); services.AddSingleton<INavigationService, NavigationService>(); // ViewModels services.AddTransient<CustomersViewModel>(); services.AddTransient<CustomerDetailViewModel>(); // Views services.AddTransient<MainWindow>(); services.AddTransient<CustomersView>(); }) .Build(); } protected override async void OnStartup(StartupEventArgs e) { await _host.StartAsync(); var mainWindow = _host.Services.GetRequiredService<MainWindow>(); mainWindow.Show(); base.OnStartup(e); } protected override async void OnExit(ExitEventArgs e) { await _host.StopAsync(); _host.Dispose(); base.OnExit(e); } } ``` ## Data Binding Modes ```xml <!-- OneTime: Read once at initialization --> <TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/> <!-- OneWay: Source to target only (default for most properties) --> <TextBlock Text="{Binding Name, Mode=OneWay}"/> <!-- TwoWay: Bidirectional synchronization --> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <!-- OneWayToSource: Target to source only --> <TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/> ``` ## Value Converters ```csharp public class BooleanToVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is bool boolValue) { return boolValue ? Visibility.Visible : Visibility.Collapsed; } return Visibility.Collapsed; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value is Visibility.Visible; } } // Multi-value converter public class MultiplyConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { if (values.Length == 2 && values[0] is double a && values[1] is double b) { return a * b; } return 0.0; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } ``` ## Styles and Templates ### Resource Dictionary ```xml <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!-- Implicit style for all Buttons --> <Style TargetType="Button"> <Setter Property="Padding" Value="10,5"/> <Setter Property="Margin" Value="5"/> <Setter Property="Background" Value="#0078D4"/> <Setter Property="Foreground" Value="White"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Border Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}"> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- Named style --> <Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}"> <Setter Property="Background" Value="#D32F2F"/> </Style> </ResourceDictionary> ``` ## Threading and Dispatcher ```csharp // Update UI from background thread await Task.Run(async () => { var data = await LoadDataAsync(); // Must use Dispatcher to update UI Application.Current.Dispatcher.Invoke(() => { Items.Clear(); foreach (var item in data) { Items.Add(item); } }); }); // Better: Use async/await properly private async Task LoadDataAsync() { IsLoading = true; try { // This runs on background thread var data = await _service.GetDataAsync(); // This automatically marshals to UI thread Items = new ObservableCollection<Item>(data); } finally { IsLoading = false; } } ``` ## Anti-Patterns to Avoid | Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Logic in code-behind | Hard to test, tight coupling | Use MVVM with ViewModels | | Synchronous blocking calls | UI freezes | Use async/await | | Manual INotifyPropertyChanged | Boilerplate, error-prone | Use MVVM Toolkit attributes | | Hardcoded colors/sizes | Inconsistent, hard to theme | Use resource dictionaries | | Direct Dispatcher.Invoke everywhere | Complex, error-prone | Prefer async/await marshaling | | God ViewModel | Unmaintainable | Split into focused ViewModels | | Skipping binding validation | Runtime errors hidden | Use ValidatesOnDataErrors | | Event handlers for everything | Memory leaks, coupling | Use commands and bindings | ## Best Practices 1. **Use compiled bindings in .NET 5+:** - Enable `x:CompileBindings="True"` for performance 2. **Implement INotifyDataErrorInfo for validation:** ```csharp [ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] private string _name = string.Empty; ``` 3. **Use weak event patterns for long-lived subscriptions:** ```csharp WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler); ``` 4. **Virtualize large collections:** ```xml <ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/> ``` 5. **Freeze Freezables when possible:** ```csharp var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // Thread-safe, better performance ``` 6. **Use design-time data:** ```xml <Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}"> ``` ## Testing ```csharp [Fact] public async Task RefreshCommand_LoadsCustomers() { var mockService = new Mock<ICustomerService>(); mockService.Setup(s => s.GetAllAsync()) .ReturnsAsync(new[] { new Customer { Name = "Test" } }); var viewModel = new CustomersViewModel(mockService.Object); await viewModel.RefreshCommand.ExecuteAsync(null); Assert.Single(viewModel.Customers); Assert.Equal("Test", viewModel.Customers[0].Name); } [Fact] public void SaveCommand_CannotExecute_WhenNoSelection() { var mockService = new Mock<ICustomerService>(); var viewModel = new CustomersViewModel(mockService.Object); viewModel.SelectedCustomer = null; Assert.False(viewModel.SaveCommand.CanExecute(null)); } ``` ## Deliver - cleaner WPF views and view-model boundaries - safer binding and threading behavior - migration guidance grounded in actual Windows constraints - MVVM pattern with testable ViewModels ## Validate - binding and command flows are explicit - code-behind is not carrying hidden business logic - Windows-only assumptions are acknowledged - threading and dispatcher usage is correct - styles and resources are properly organized
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.