Claude Skill

dotnet-wpf

Build and modernize WPF applications on .NET with correct XAML, data binding, commands, threading, styling, and Windows desktop migration decisions.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-wpf-bfa4ebd.zip · 13 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

  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

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 performance
    
  6. 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
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.

No comments yet.

Reviews (0)

No reviews yet.

Related