
Wpf
- 40 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
wpf is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- wpf
- AI & Agent Building
- AI-coding skill
Wpf by the numbers
- 40 all-time installs (skills.sh)
- +5 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #8,215 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill wpfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
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
Current Upstream Notes
- The refreshed WPF overview page reiterates WPF as a Windows desktop UI stack with XAML, data binding, styling, templates, resources, and vector/rich-media composition. Keep WPF-specific guidance separate from WinUI or MAUI unless the task is explicitly a migration or comparison.
- For modernization work, check both
.NET Frameworkcompatibility constraints and current .NET desktop migration docs before moving project files or XAML resource dictionaries.
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
1. Use compiled bindings in .NET 5+:
- Enable
x:CompileBindings="True"for performance
2. 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;3. Use weak event patterns for long-lived subscriptions:
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler);4. Virtualize large collections:
<ListBox VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
ItemsSource="{Binding LargeCollection}"/>5. Freeze Freezables when possible:
var brush = new SolidColorBrush(Colors.Blue);
brush.Freeze(); // Thread-safe, better performance6. Use 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
{
"version": "1.0.1",
"category": "Desktop",
"package_prefix": "Microsoft.WindowsDesktop.App.WPF"
}
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.
// 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.
// 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.
// 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.
// 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.
// 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.
// 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.
// WRONG: No change notification
public class Person
{
public string Name { get; set; } = string.Empty; // UI won't update
}Solution: Use ObservableObject or implement INotifyPropertyChanged.
// 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.
// 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.
// 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.
<!-- 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.
<!-- 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.
<!-- WRONG: Typo in binding path fails silently -->
<TextBlock Text="{Binding Nmae}"/> <!-- Should be "Name" -->Solution: Enable binding diagnostics and validate at design time.
<!-- Enable detailed binding errors in App.xaml -->
<Application xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase">
<!-- In resources or startup code -->
</Application>// In App.xaml.cs
#if DEBUG
PresentationTraceSources.DataBindingSource.Listeners.Add(new ConsoleTraceListener());
PresentationTraceSources.DataBindingSource.Switch.Level = SourceLevels.Warning;
#endifThreading Mistakes
Blocking UI Thread
Problem: Synchronous operations freeze the UI.
// 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.
// 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.
// 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.
// 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 automaticallyNested Dispatcher.Invoke Calls
Problem: Excessive Dispatcher.Invoke calls hurt performance.
// WRONG: Multiple dispatcher calls
foreach (var item in items)
{
Application.Current.Dispatcher.Invoke(() =>
{
Items.Add(item); // Called for each item
});
}Solution: Batch UI updates.
// 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.
// 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.
// 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.
// WRONG: Binding to plain object
public class Person
{
public string Name { get; set; } // No INPC
}
// View binds to this, WPF creates strong referenceSolution: Always implement INotifyPropertyChanged for bound objects.
// 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.
// WRONG: Static event
public static class EventAggregator
{
public static event EventHandler? SomethingHappened;
}
// Subscriber never gets collected
EventAggregator.SomethingHappened += OnSomethingHappened;Solution: Use weak reference messaging.
// 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.
<!-- WRONG: Hardcoded values -->
<Button Background="#0078D4"
FontSize="14"
Padding="10,5"
Foreground="White"/>Solution: Use resource dictionaries.
<!-- 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.
<!-- 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.
<!-- 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.
<!-- WRONG: Loses default Button styling -->
<Style x:Key="RedButton" TargetType="Button">
<Setter Property="Background" Value="Red"/>
</Style>Solution: Use BasedOn.
<!-- 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.
<!-- WRONG: No virtualization -->
<ItemsControl ItemsSource="{Binding ThousandsOfItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/> <!-- StackPanel doesn't virtualize -->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>Solution: Enable virtualization.
<!-- 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.
// 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.
// 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.
// 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.
// 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.
// WRONG: async void
private async void SaveCommand_Execute()
{
await _service.SaveAsync(); // Exception here is lost
}Solution: Use proper async command patterns.
// CORRECT: MVVM Toolkit async command
[RelayCommand]
private async Task SaveAsync()
{
await _service.SaveAsync();
}Not Calling CanExecuteChanged
Problem: Command button stays enabled/disabled incorrectly.
// 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.
// 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.
<!-- 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.
// 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.
// WRONG: Save without validation check
[RelayCommand]
private async Task SaveAsync()
{
await _service.SaveAsync(CurrentItem); // May save invalid data
}Solution: Always validate before save.
// 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;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/RepositoriesViewModel Base Class with MVVM Toolkit
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
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
// 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
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
// 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
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
<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
<!-- 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
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)
};
}
}<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
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
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;
}
}
}<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
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
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
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();
}
}
}<TextBox local:TextBoxBehaviors.SelectAllOnFocus="True"/>Validation Pattern with INotifyDataErrorInfo
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;
}<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
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;
}
}
}<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>