# WPF UI Implementation Guide
Step-by-step guides for common implementation tasks in the WPF UI project.
## Table of Contents
1. [How to Add a New Control](#1-how-to-add-a-new-control)
2. [How to Add a New Service](#2-how-to-add-a-new-service)
3. [How to Add a Gallery Page](#3-how-to-add-a-gallery-page)
4. [How to Use Win32 Interop](#4-how-to-use-win32-interop)
5. [How to Add Theme Support](#5-how-to-add-theme-support)
6. [Testing a Feature](#6-testing-a-feature)
---
## 1. How to Add a New Control
This guide walks through creating a new control following the WPF UI conventions.
### Step 1: Create Control Folder Structure
```
src/Wpf.Ui/Controls/
└── MyControl/
├── MyControl.cs # Code-behind class
└── MyControl.xaml # Style and ControlTemplate
```
### Step 2: Create the Control Class (MyControl.cs)
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System.Windows;
using System.Windows.Controls;
// ReSharper disable once CheckNamespace
namespace Wpf.Ui.Controls;
///
/// A custom control that demonstrates the WPF UI control pattern.
///
///
///
/// <ui:MyControl
/// Content="Hello World"
/// Appearance="Primary"
/// Icon="{ui:SymbolIcon Symbol=Heart24}" />
///
///
public class MyControl : ContentControl, IAppearanceControl, IIconControl
{
/// Identifies the dependency property.
public static readonly DependencyProperty AppearanceProperty = DependencyProperty.Register(
nameof(Appearance),
typeof(ControlAppearance),
typeof(MyControl),
new PropertyMetadata(ControlAppearance.Primary)
);
/// Identifies the dependency property.
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
nameof(Icon),
typeof(IconElement),
typeof(MyControl),
new PropertyMetadata(null, null, IconElement.Coerce)
);
/// Identifies the dependency property.
public static readonly DependencyProperty IsCustomProperty = DependencyProperty.Register(
nameof(IsCustom),
typeof(bool),
typeof(MyControl),
new PropertyMetadata(false, OnIsCustomChanged)
);
static MyControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MyControl),
new FrameworkPropertyMetadata(typeof(MyControl))
);
}
///
/// Gets or sets the appearance of the control.
///
[Bindable(true)]
[Category("Appearance")]
public ControlAppearance Appearance
{
get => (ControlAppearance)GetValue(AppearanceProperty);
set => SetValue(AppearanceProperty, value);
}
///
/// Gets or sets the icon displayed in the control.
///
[Bindable(true)]
[Category("Appearance")]
public IconElement? Icon
{
get => (IconElement?)GetValue(IconProperty);
set => SetValue(IconProperty, value);
}
///
/// Gets or sets a value indicating whether the control uses custom behavior.
///
[Bindable(true)]
[Category("Behavior")]
public bool IsCustom
{
get => (bool)GetValue(IsCustomProperty);
set => SetValue(IsCustomProperty, value);
}
private static void OnIsCustomChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not MyControl control)
{
return;
}
control.OnIsCustomChanged((bool)e.NewValue);
}
protected virtual void OnIsCustomChanged(bool isCustom)
{
// Handle property change
}
}
```
### Step 3: Create the XAML Style (MyControl.xaml)
```xml
11,5,11,6
1
0,0,8,0
32
```
### Step 4: Register in Wpf.Ui.xaml
Add reference to `src/Wpf.Ui/Resources/Wpf.Ui.xaml`:
```xml
```
### Step 5: Add Designer Support (Optional)
Add toolbox icon bitmap to `src/Wpf.Ui/Assets/Toolbox/MyControl.bmp` (16x16 pixels).
Register in `src/Wpf.Ui/VisualStudioToolsManifest.xml`:
```xml
```
### Step 6: Add to Category Documentation
Document the control category in architecture notes for future reference.
---
## 2. How to Add a New Service
This guide shows how to create a new service following the WPF UI service pattern.
### Step 1: Create the Service Interface (IMyService.cs)
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
namespace Wpf.Ui;
///
/// Provides functionality for managing custom operations.
///
public interface IMyService
{
///
/// Gets the current state of the service.
///
bool IsActive { get; }
///
/// Initializes the service with the specified parameter.
///
/// The initialization parameter.
void Initialize(string parameter);
///
/// Performs an asynchronous operation.
///
/// Cancellation token.
/// A task representing the asynchronous operation.
Task PerformOperationAsync(CancellationToken cancellationToken = default);
}
```
### Step 2: Create the Service Implementation (MyService.cs)
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Wpf.Ui;
///
/// Implementation of .
///
public partial class MyService : IMyService
{
private string? _parameter;
private bool _isActive;
///
public bool IsActive => _isActive;
///
public void Initialize(string parameter)
{
ArgumentNullException.ThrowIfNull(parameter);
_parameter = parameter;
_isActive = true;
}
///
public async Task PerformOperationAsync(CancellationToken cancellationToken = default)
{
ThrowIfNotInitialized();
try
{
// Perform async operation
await Task.Delay(100, cancellationToken);
return true;
}
catch (OperationCanceledException)
{
return false;
}
}
private void ThrowIfNotInitialized()
{
if (!_isActive || _parameter is null)
{
throw new InvalidOperationException("Service has not been initialized. Call Initialize() first.");
}
}
}
```
### Step 3: Add DI Extension Method
Create `src/Wpf.Ui.DependencyInjection/ServiceCollectionExtensions.MyService.cs`:
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using Microsoft.Extensions.DependencyInjection;
using Wpf.Ui;
namespace Wpf.Ui.DependencyInjection;
///
/// Extension methods for registering WPF UI services.
///
public static partial class ServiceCollectionExtensions
{
///
/// Registers as a singleton.
///
/// The service collection.
/// The service collection for chaining.
public static IServiceCollection AddMyService(this IServiceCollection services)
{
_ = services.AddSingleton();
return services;
}
}
```
### Step 4: Register in Application
```csharp
// In your application startup (e.g., App.xaml.cs or Program.cs)
services.AddMyService();
```
### Step 5: Use the Service
```csharp
public partial class MyViewModel : ViewModel
{
private readonly IMyService _myService;
public MyViewModel(IMyService myService)
{
_myService = myService;
_myService.Initialize("parameter-value");
}
[RelayCommand]
private async Task OnPerformAction()
{
bool result = await _myService.PerformOperationAsync();
// Handle result
}
}
```
---
## 3. How to Add a Gallery Page
This guide demonstrates adding a new demo page to the WPF UI Gallery application.
### Step 1: Create ViewModel
Create `src/Wpf.Ui.Gallery/ViewModels/Pages/MyCategory/MyControlViewModel.cs`:
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Wpf.Ui.Controls;
namespace Wpf.Ui.Gallery.ViewModels.Pages.MyCategory;
public partial class MyControlViewModel : ViewModel
{
[ObservableProperty]
private string _textValue = "Hello World";
[ObservableProperty]
private bool _isEnabled = true;
[ObservableProperty]
private ControlAppearance _selectedAppearance = ControlAppearance.Primary;
[ObservableProperty]
private ObservableCollection _appearances = new()
{
ControlAppearance.Primary,
ControlAppearance.Secondary,
ControlAppearance.Success,
ControlAppearance.Danger
};
[RelayCommand]
private void OnToggleEnabled()
{
IsEnabled = !IsEnabled;
}
[RelayCommand]
private async Task OnPerformAction()
{
await Task.Delay(500);
TextValue = "Action performed!";
}
public override Task OnNavigatedToAsync()
{
// Called when page is navigated to
return base.OnNavigatedToAsync();
}
public override Task OnNavigatedFromAsync()
{
// Called when navigating away from page
return base.OnNavigatedFromAsync();
}
}
```
### Step 2: Create Page (XAML)
Create `src/Wpf.Ui.Gallery/Views/Pages/MyCategory/MyControlPage.xaml`:
```xml
Demonstrates the MyControl with various appearance modes and interactions.
]]>
```
### Step 3: Create Page Code-Behind
Create `src/Wpf.Ui.Gallery/Views/Pages/MyCategory/MyControlPage.xaml.cs`:
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using Wpf.Ui.Controls;
using Wpf.Ui.Gallery.Attributes;
using Wpf.Ui.Gallery.ViewModels.Pages.MyCategory;
namespace Wpf.Ui.Gallery.Views.Pages.MyCategory;
[GalleryPage("Custom control demonstration.", SymbolRegular.Heart24)]
public partial class MyControlPage : INavigableView
{
public MyControlViewModel ViewModel { get; }
public MyControlPage(MyControlViewModel viewModel)
{
ViewModel = viewModel;
DataContext = this;
InitializeComponent();
}
}
```
### Step 4: Register in DI Container
Add to `src/Wpf.Ui.Gallery/App.xaml.cs` or DI configuration:
```csharp
// Register ViewModel
services.AddSingleton();
// Register Page
services.AddSingleton();
```
### Step 5: Add Navigation Entry
If you need manual navigation registration (not using automatic discovery), add to navigation setup:
```csharp
navigationService.SetNavigationControl(navigationView);
// Register page type
menuItems.Add(new NavigationViewItem
{
Content = "MyControl",
Icon = new SymbolIcon { Symbol = SymbolRegular.Heart24 },
TargetPageType = typeof(MyControlPage)
});
```
---
## 4. How to Use Win32 Interop
This guide demonstrates the three-layer Win32 interop architecture used in WPF UI.
### Step 1: Add Function to NativeMethods.txt
Edit `src/Wpf.Ui/NativeMethods.txt`:
```text
// Existing entries...
DwmSetWindowAttribute
IsWindow
// Add your new function
GetSystemMetrics
```
This triggers CsWin32 to auto-generate P/Invoke declarations in the `Windows.Win32` namespace.
### Step 2: Create Managed Wrapper (if needed)
If you need a safe wrapper with handle validation, add to `src/Wpf.Ui/Interop/UnsafeNativeMethods.cs`:
```csharp
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Wpf.Ui.Interop;
internal static partial class UnsafeNativeMethods
{
///
/// Safely retrieves a system metric value with handle validation.
///
/// Window handle for context (optional).
/// The system metric to retrieve.
/// The metric value, or 0 if the operation fails.
public static int GetSystemMetricSafe(IntPtr handle, SYSTEM_METRICS_INDEX metric)
{
// Validate handle if provided
if (handle != IntPtr.Zero && !PInvoke.IsWindow(new HWND(handle)))
{
return 0;
}
try
{
return PInvoke.GetSystemMetrics(metric);
}
catch
{
// Swallow exception for OS compatibility (intentional)
return 0;
}
}
}
```
### Step 3: Create Utility Helper (if needed)
If you need higher-level utility functions, add to `src/Wpf.Ui/Win32/Utilities.cs`:
```csharp
using Wpf.Ui.Interop;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Wpf.Ui.Win32;
internal sealed class Utilities
{
///
/// Gets the width of the window border.
///
public static int BorderWidth => UnsafeNativeMethods.GetSystemMetricSafe(
IntPtr.Zero,
SYSTEM_METRICS_INDEX.SM_CXBORDER
);
///
/// Gets the height of the window border.
///
public static int BorderHeight => UnsafeNativeMethods.GetSystemMetricSafe(
IntPtr.Zero,
SYSTEM_METRICS_INDEX.SM_CYBORDER
);
}
```
### Step 4: Use in Your Code
```csharp
using Wpf.Ui.Interop;
using Wpf.Ui.Win32;
using Windows.Win32;
using Windows.Win32.Foundation;
// Option 1: Direct P/Invoke (for simple cases)
int screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
// Option 2: Safe wrapper (validates handle, catches exceptions)
int borderWidth = UnsafeNativeMethods.GetSystemMetricSafe(
windowHandle,
SYSTEM_METRICS_INDEX.SM_CXBORDER
);
// Option 3: High-level utility
int borderHeight = Utilities.BorderHeight;
```
### Step 5: Handle WndProc Messages (if needed)
To intercept Windows messages:
```csharp
using System.Windows.Interop;
using Windows.Win32.UI.WindowsAndMessaging;
public class MyWindow : Window
{
private HwndSource? _hwndSource;
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_hwndSource = (HwndSource)PresentationSource.FromVisual(this);
_hwndSource?.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
switch ((uint)msg)
{
case PInvoke.WM_DWMCOLORIZATIONCOLORCHANGED:
// Handle theme change
handled = true;
break;
case PInvoke.WM_THEMECHANGED:
// Handle system theme change
handled = true;
break;
}
return IntPtr.Zero;
}
protected override void OnClosed(EventArgs e)
{
_hwndSource?.RemoveHook(WndProc);
base.OnClosed(e);
}
}
```
### Important Notes
1. **Always validate handles** before calling Win32 APIs:
```csharp
if (handle == IntPtr.Zero || !PInvoke.IsWindow(new HWND(handle)))
return false;
```
2. **Catch exceptions** in interop code for OS compatibility (some APIs may not exist on older Windows versions)
3. **Use `unsafe` keyword** for pointer operations
4. **Check OS version** before using new APIs:
```csharp
if (Wpf.Ui.Win32.Utilities.IsOSWindows11OrNewer)
{
// Use Windows 11-specific API
}
```
---
## 5. How to Add Theme Support
This guide shows how to make your control theme-aware.
### Step 1: Use DynamicResource in XAML
```xml
```
### Step 2: Subscribe to Theme Changes (if needed)
If your control needs to react to theme changes programmatically:
```csharp
using Wpf.Ui.Appearance;
public class MyControl : ContentControl
{
public MyControl()
{
ApplicationThemeManager.Changed += OnThemeChanged;
}
private void OnThemeChanged(ApplicationTheme currentTheme, Color systemAccent)
{
// React to theme change
UpdateVisuals(currentTheme);
}
private void UpdateVisuals(ApplicationTheme theme)
{
// Update control state based on theme
if (theme == ApplicationTheme.Dark)
{
// Dark theme-specific logic
}
else
{
// Light theme-specific logic
}
}
}
```
### Step 3: Use Theme-Aware Brushes
Available dynamic brush resources:
**Background Brushes**:
- `ApplicationBackgroundBrush`
- `ControlFillColorDefaultBrush`
- `ControlFillColorSecondaryBrush`
- `ControlFillColorTertiaryBrush`
- `ControlFillColorDisabledBrush`
**Foreground Brushes**:
- `TextFillColorPrimaryBrush`
- `TextFillColorSecondaryBrush`
- `TextFillColorTertiaryBrush`
- `TextFillColorDisabledBrush`
**Accent Brushes**:
- `AccentFillColorDefaultBrush`
- `AccentFillColorSecondaryBrush`
- `AccentFillColorTertiaryBrush`
- `SystemAccentColor` (Color, not Brush)
**Border Brushes**:
- `ControlElevationBorderBrush`
- `ControlStrokeColorDefaultBrush`
- `ControlStrokeColorSecondaryBrush`
### Step 4: Implement IThemeControl (if applicable)
```csharp
public class MyControl : ContentControl, IThemeControl
{
/// Identifies the dependency property.
public static readonly DependencyProperty ThemeProperty = DependencyProperty.Register(
nameof(Theme),
typeof(ApplicationTheme),
typeof(MyControl),
new PropertyMetadata(ApplicationTheme.Light, OnThemeChanged)
);
///
/// Gets or sets the theme of the control.
///
public ApplicationTheme Theme
{
get => (ApplicationTheme)GetValue(ThemeProperty);
set => SetValue(ThemeProperty, value);
}
private static void OnThemeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyControl control)
{
control.UpdateTheme((ApplicationTheme)e.NewValue);
}
}
private void UpdateTheme(ApplicationTheme theme)
{
// Update control based on theme
}
}
```
### Step 5: Test Theme Switching
```csharp
// Apply theme programmatically
ApplicationThemeManager.Apply(ApplicationTheme.Dark);
// Apply system accent color
ApplicationAccentColorManager.ApplySystemAccent();
// Watch for system theme changes
SystemThemeWatcher.Watch(myWindow);
```
---
## 6. Testing a Feature
This guide covers both unit testing and integration testing for WPF UI.
### Unit Test Example
Create `tests/Wpf.Ui.UnitTests/Controls/MyControlTests.cs`:
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using System.Windows;
using NSubstitute;
using Wpf.Ui.Controls;
using Xunit;
namespace Wpf.Ui.UnitTests.Controls;
public class MyControlTests
{
[Fact]
public void Constructor_ShouldSetDefaultValues()
{
// Arrange & Act
var control = new MyControl();
// Assert
Assert.Equal(ControlAppearance.Primary, control.Appearance);
Assert.Null(control.Icon);
Assert.False(control.IsCustom);
}
[Fact]
public void Appearance_ShouldUpdateWhenSet()
{
// Arrange
var control = new MyControl();
// Act
control.Appearance = ControlAppearance.Secondary;
// Assert
Assert.Equal(ControlAppearance.Secondary, control.Appearance);
}
[Fact]
public void IsCustomChanged_ShouldInvokeCallback_WhenValueChanges()
{
// Arrange
var control = new MyControl();
bool callbackInvoked = false;
// Subscribe to property change if there's a routed event
// Or test via reflection if the method is protected
// Act
control.IsCustom = true;
// Assert
Assert.True(control.IsCustom);
}
[Fact]
public void Icon_ShouldAcceptSymbolIcon()
{
// Arrange
var control = new MyControl();
var icon = new SymbolIcon { Symbol = SymbolRegular.Heart24 };
// Act
control.Icon = icon;
// Assert
Assert.Equal(icon, control.Icon);
}
}
```
### Integration Test Example
Create `tests/Wpf.Ui.Gallery.IntegrationTests/MyControlTests.cs`:
```csharp
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
// All Rights Reserved.
using AwesomeAssertions.Autofac;
using FlaUI.Core.AutomationElements;
using Wpf.Ui.Gallery.IntegrationTests.Fixtures;
using Xunit;
namespace Wpf.Ui.Gallery.IntegrationTests;
public sealed class MyControlTests : UiTest
{
[Fact]
public async Task MyControl_ShouldBeVisible_WhenPageLoads()
{
// Arrange
await NavigateToPage("MyControl");
// Act
var control = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("MyControlExample"));
// Assert
control.Should().NotBeNull();
control.IsOffscreen.Should().BeFalse();
}
[Fact]
public async Task MyControl_ShouldChangeAppearance_WhenComboBoxChanged()
{
// Arrange
await NavigateToPage("MyControl");
var comboBox = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("AppearanceComboBox")).AsComboBox();
var control = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("MyControlExample"));
// Act
comboBox.Select(1); // Select "Secondary"
await Task.Delay(100);
// Assert
control.Should().NotBeNull();
// Add appearance-specific assertions
}
[Fact]
public async Task MyControl_ShouldDisable_WhenToggleSwitchUnchecked()
{
// Arrange
await NavigateToPage("MyControl");
var toggleSwitch = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("EnabledToggle")).AsToggleButton();
var control = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("MyControlExample"));
// Act
toggleSwitch.Toggle();
await Task.Delay(100);
// Assert
control.IsEnabled.Should().BeFalse();
}
[Fact]
public async Task MyControl_ShouldExecuteCommand_WhenButtonClicked()
{
// Arrange
await NavigateToPage("MyControl");
var button = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("ActionButton")).AsButton();
var textBlock = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("ResultText")).AsLabel();
// Act
button.Click();
await Task.Delay(600); // Wait for async operation
// Assert
textBlock.Text.Should().Be("Action performed!");
}
private async Task NavigateToPage(string pageName)
{
var navigationView = Window.FindFirstDescendant(cf =>
cf.ByAutomationId("NavigationView"));
var menuItem = navigationView.FindFirstDescendant(cf =>
cf.ByName(pageName));
menuItem.Click();
await Task.Delay(500); // Wait for navigation
}
}
```
### Test Infrastructure
**Base Test Class** (`tests/Wpf.Ui.Gallery.IntegrationTests/Fixtures/UiTest.cs`):
```csharp
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using FlaUI.Core;
using FlaUI.Core.AutomationElements;
using FlaUI.UIA3;
using Xunit;
namespace Wpf.Ui.Gallery.IntegrationTests.Fixtures;
public abstract class UiTest : IAsyncLifetime
{
protected Application? Application { get; private set; }
protected Window Window { get; private set; } = null!;
private Process? _process;
public async Task InitializeAsync()
{
// Launch Gallery application
var appPath = GetApplicationPath();
_process = Process.Start(appPath);
await Task.Delay(2000); // Wait for app to start
// Attach to application
Application = FlaUI.Core.Application.Attach(_process);
using var automation = new UIA3Automation();
Window = Application.GetMainWindow(automation);
}
public async Task DisposeAsync()
{
Application?.Close();
Application?.Dispose();
if (_process != null && !_process.HasExited)
{
_process.Kill();
_process.Dispose();
}
await Task.CompletedTask;
}
private static string GetApplicationPath()
{
// Path to Gallery executable
return Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"..\\..\\..\\..\\..\\src\\Wpf.Ui.Gallery\\bin\\Debug\\net10.0-windows\\Wpf.Ui.Gallery.exe"
);
}
}
```
### Running Tests
```bash
# Run all tests
dotnet test
# Run only unit tests
dotnet test tests/Wpf.Ui.UnitTests/Wpf.Ui.UnitTests.csproj
# Run only integration tests
dotnet test tests/Wpf.Ui.Gallery.IntegrationTests/Wpf.Ui.Gallery.IntegrationTests.csproj
# Run with code coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
```
---
## Common Pitfalls to Avoid
1. **Don't add package versions to .csproj** - use `Directory.Packages.props` only
2. **Don't forget handle validation** in Win32 interop:
```csharp
if (handle == IntPtr.Zero || !PInvoke.IsWindow(new HWND(handle)))
return false;
```
3. **Don't use `var` for non-apparent types** (causes warning)
4. **Don't forget MIT license header** (causes error via IDE0073)
5. **Don't modify control namespace** to match folder structure - use flat `Wpf.Ui.Controls`
6. **Don't use StaticResource for theme-dependent values** - use DynamicResource
7. **Don't forget to register pages and ViewModels** in DI container
8. **Don't use `this.` qualification** (SA1101 suppressed)
9. **Don't add XML docs that restate code** - explain WHY, not WHAT
10. **Don't skip the `[GalleryPage]` attribute** on Gallery pages
---
## Quick Reference Checklist
### Adding a Control
- [ ] Create folder under `Controls/`
- [ ] Create `.cs` file with control class
- [ ] Create `.xaml` file with style
- [ ] Implement interfaces (IAppearanceControl, IIconControl)
- [ ] Register DependencyProperties with XML docs
- [ ] Override DefaultStyleKeyProperty
- [ ] Add to `Wpf.Ui.xaml` MergedDictionaries
- [ ] Add toolbox icon (optional)
- [ ] Add unit tests
### Adding a Service
- [ ] Create `I{Name}Service.cs` interface
- [ ] Create `{Name}Service.cs` implementation
- [ ] Add DI extension method in `Wpf.Ui.DependencyInjection`
- [ ] Add XML docs with examples
- [ ] Add unit tests
- [ ] Document in CLAUDE.md
### Adding a Gallery Page
- [ ] Create ViewModel in `ViewModels/Pages/{Category}/`
- [ ] Create Page.xaml in `Views/Pages/{Category}/`
- [ ] Create Page.xaml.cs code-behind
- [ ] Add `[GalleryPage]` attribute
- [ ] Register ViewModel and Page in DI
- [ ] Add navigation entry (if manual)
- [ ] Add code examples with syntax:CodeBlock