更新客户端渲染,更新了壳
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
# ADR-001: Multi-Target Framework Support
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The WPF UI library needs to support a wide range of .NET implementations to maximize compatibility with existing projects while leveraging modern .NET features where available.
|
||||
|
||||
### Supported Frameworks
|
||||
- **.NET 10, 9, 8** - Modern .NET with Windows-specific APIs
|
||||
- **.NET Framework 4.8.1, 4.7.2, 4.6.2** - Legacy enterprise applications
|
||||
- **.NET Standard 2.0, 2.1** - Abstractions library only (maximizes compatibility)
|
||||
|
||||
## Decision
|
||||
|
||||
### Core Library (Wpf.Ui)
|
||||
Target frameworks: `net10.0-windows;net9.0-windows;net8.0-windows;net481;net472;net462`
|
||||
|
||||
**Rationale:**
|
||||
- Windows-specific project requires `-windows` TFM suffix for .NET 5+
|
||||
- .NET Framework support ensures compatibility with legacy WPF applications
|
||||
- Multi-version .NET support provides upgrade path for consumers
|
||||
|
||||
### Abstractions Library (Wpf.Ui.Abstractions)
|
||||
Target frameworks: `net10.0;net9.0;net8.0;net462;netstandard2.1;netstandard2.0`
|
||||
|
||||
**Rationale:**
|
||||
- No WPF dependencies allows broader compatibility
|
||||
- .NET Standard 2.0 enables use in class libraries shared between .NET Framework and .NET Core/5+
|
||||
- AOT-compatible (aot_compatible: true) for modern deployment scenarios
|
||||
|
||||
### DI Integration (Wpf.Ui.DependencyInjection)
|
||||
Target frameworks: Same as Abstractions
|
||||
|
||||
**Rationale:**
|
||||
- Depends only on Microsoft.Extensions.DependencyInjection.Abstractions (version 3.1.0 for broad compatibility)
|
||||
- No WPF dependencies
|
||||
- Enables DI integration in non-WPF contexts (e.g., background services)
|
||||
|
||||
## Central Package Management
|
||||
|
||||
All package versions are managed in `Directory.Packages.props`:
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Package versions defined here -->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.242" />
|
||||
<PackageVersion Include="System.Memory" Version="4.6.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
Individual projects reference packages without version attributes:
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" />
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Single source of truth for package versions
|
||||
- Prevents version conflicts across projects
|
||||
- Simplifies dependency updates
|
||||
|
||||
## Conditional Compilation
|
||||
|
||||
### Framework Detection
|
||||
```csharp
|
||||
#if NET5_0_OR_GREATER
|
||||
// Modern .NET APIs (Environment.OSVersion)
|
||||
#else
|
||||
// .NET Framework fallback (registry)
|
||||
#endif
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
// DisposeAsync for CancellationTokenRegistration
|
||||
#endif
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
// Latest .NET 8+ features
|
||||
#endif
|
||||
```
|
||||
|
||||
### Framework-Specific Dependencies
|
||||
```xml
|
||||
<!-- Only for .NET Framework 4.6.2 -->
|
||||
<PackageReference Include="System.ValueTuple" Condition="'$(TargetFramework)' == 'net462'" />
|
||||
|
||||
<!-- Only for .NET Core/5+ (not .NET Framework 4.6.2) -->
|
||||
<PackageReference Include="System.Drawing.Common" Condition="'$(TargetFramework)' != 'net462'" />
|
||||
```
|
||||
|
||||
## PolySharp Integration
|
||||
|
||||
**Package:** PolySharp (build-time source generator)
|
||||
|
||||
Provides polyfills for newer C# features on older target frameworks:
|
||||
- C# 11+ features on .NET 6/7/8
|
||||
- C# 12+ features on older .NET versions
|
||||
|
||||
**Configuration:**
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<CompilerVisibleProperty Include="PolySharpExcludeGeneratedTypes" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Exclude specific polyfills if needed -->
|
||||
<PolySharpExcludeGeneratedTypes>
|
||||
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute;
|
||||
System.Diagnostics.CodeAnalysis.UnscopedRefAttribute
|
||||
</PolySharpExcludeGeneratedTypes>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Use modern C# syntax across all target frameworks
|
||||
- Init-only properties on .NET Framework
|
||||
- Required members support
|
||||
- CallerArgumentExpression on older frameworks
|
||||
|
||||
## Language Version
|
||||
|
||||
Set to C# 14.0 across all projects:
|
||||
```xml
|
||||
<LangVersion>14.0</LangVersion>
|
||||
```
|
||||
|
||||
> **Note:** `Wpf.Ui.csproj` overrides this to `<LangVersion>preview</LangVersion>` to enable C# preview features.
|
||||
|
||||
**Combined with PolySharp**, this enables:
|
||||
- Latest C# language features
|
||||
- Polyfills generated at compile-time for older frameworks
|
||||
- No runtime dependencies
|
||||
|
||||
## Enforcement
|
||||
|
||||
### MUST Follow
|
||||
|
||||
1. **All package versions in `Directory.Packages.props` only** — Central Package Management is the single source of truth for NuGet versions
|
||||
2. **Use `-windows` TFM suffix** for projects with WPF dependencies (e.g., `net10.0-windows`, not `net10.0`)
|
||||
3. **Use `netstandard2.0`/`netstandard2.1`** for abstraction-only packages that have no WPF or Windows dependency
|
||||
4. **Guard framework-specific code with `#if NET{X}_0_OR_GREATER` directives** — never use runtime version checks for compile-time API differences
|
||||
5. **Use PolySharp for C# polyfills** — never hand-write polyfill classes for language features (e.g., `IsExternalInit`, `CallerArgumentExpression`)
|
||||
6. **Set `LangVersion` in `Directory.Build.props`** — override in individual `.csproj` only when justified (currently only `Wpf.Ui.csproj` overrides to `preview`)
|
||||
|
||||
### MUST NOT Do
|
||||
|
||||
1. **Never add `Version` attribute to `PackageReference`** in `.csproj` files — all versions must be in `Directory.Packages.props`
|
||||
2. **Never add a new TFM without updating all projects** that share the same TFM set — all projects in a TFM group must stay in sync
|
||||
3. **Never use `#if` with specific patch versions** (e.g., `NET8_0_10`) — only use `_OR_GREATER` suffixed symbols
|
||||
4. **Never remove a TFM from a shipping package** without a major version bump — removing a TFM is a breaking change for consumers on that framework
|
||||
|
||||
### Verification
|
||||
|
||||
- **CPM enforcement:** `<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>` in `Directory.Build.props` causes build errors if `Version` is specified in `.csproj`
|
||||
- **TFM validation:** Build compiles all target frameworks on every `dotnet build` — missing APIs surface as compile errors immediately
|
||||
- **PolySharp coverage:** PolySharp source generator runs at build time and provides polyfills automatically; manual polyfills would cause duplicate symbol errors
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Broad Compatibility:** Supports applications from .NET Framework 4.6.2 through .NET 10
|
||||
- **Modern Development:** C# 14 preview features via PolySharp
|
||||
- **Simplified Maintenance:** Central package management
|
||||
- **Clear Upgrade Path:** Consumers can upgrade .NET version without changing library
|
||||
- **AOT Ready:** Abstractions library supports AOT scenarios
|
||||
|
||||
### Negative
|
||||
- **Increased Build Complexity:** Each commit builds 6+ framework variants
|
||||
- **Larger Package Size:** Multi-target packages include assemblies for all frameworks
|
||||
- **Conditional Compilation:** Requires `#if` directives for framework-specific code
|
||||
- **Testing Burden:** Features should be tested on multiple framework versions
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Directory.Build.props
|
||||
Central build properties defined once:
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<Version>4.2.0</Version>
|
||||
<LangVersion>14.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
### Conditional Property Groups
|
||||
```xml
|
||||
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<DefineConstants>$(DefineConstants);NET8_0_OR_GREATER</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
|
||||
<DefineConstants>$(DefineConstants);BELOW_NET8</DefineConstants>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
## References
|
||||
- [.NET Multi-Targeting Documentation](https://docs.microsoft.com/dotnet/standard/frameworks)
|
||||
- [Central Package Management](https://docs.microsoft.com/nuget/consume-packages/central-package-management)
|
||||
- [PolySharp GitHub](https://github.com/Sergio0694/PolySharp)
|
||||
- [TFM Compatibility](https://docs.microsoft.com/dotnet/standard/frameworks#net-5-os-specific-tfms)
|
||||
@@ -0,0 +1,323 @@
|
||||
# ADR-002: Control Library Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The WPF UI library provides 77+ Fluent Design System controls. The architecture must support:
|
||||
- Clear code organization
|
||||
- XAML implicit styling
|
||||
- Type-safe intellisense
|
||||
- Easy discovery for developers
|
||||
- Maintainable codebase at scale
|
||||
|
||||
## Decision
|
||||
|
||||
### Folder-Per-Control Structure
|
||||
|
||||
Each control resides in its own subfolder under `src/Wpf.Ui/Controls/{ControlName}/`:
|
||||
|
||||
```
|
||||
Controls/
|
||||
├── Button/
|
||||
│ ├── Button.cs # Control class
|
||||
│ └── Button.xaml # Implicit style ResourceDictionary
|
||||
├── NavigationView/
|
||||
│ ├── NavigationView.Base.cs # Core logic
|
||||
│ ├── NavigationView.Properties.cs # Dependency properties
|
||||
│ ├── NavigationView.Events.cs # Routed events
|
||||
│ ├── NavigationView.Navigation.cs # Navigation logic
|
||||
│ ├── NavigationView.TemplateParts.cs # Template part bindings
|
||||
│ ├── NavigationView.AttachedProperties.cs
|
||||
│ └── NavigationView.xaml # Implicit style
|
||||
└── ContentDialog/
|
||||
├── ContentDialog.cs
|
||||
├── ContentDialog.FocusBehavior.cs # Focused concern
|
||||
├── ContentDialogHost.cs # Host control
|
||||
├── ContentDialogHostBehavior.cs
|
||||
├── EventArgs/ # Supporting types
|
||||
└── ContentDialog.xaml
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Physical isolation prevents coupling between controls
|
||||
- Easy to locate all files related to a control
|
||||
- Supports partial class decomposition for complex controls
|
||||
- Clear ownership boundaries
|
||||
|
||||
### Paired .cs + .xaml Files
|
||||
|
||||
Each control consists of:
|
||||
1. **{ControlName}.cs** - Control class with code-behind
|
||||
2. **{ControlName}.xaml** - ResourceDictionary with implicit style
|
||||
|
||||
**Control Class Pattern:**
|
||||
```csharp
|
||||
// Controls/Button/Button.cs
|
||||
namespace Wpf.Ui.Controls; // Flat namespace
|
||||
// ReSharper disable once CheckNamespace
|
||||
|
||||
public class Button : System.Windows.Controls.Button, IAppearanceControl, IIconControl
|
||||
{
|
||||
static Button()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(
|
||||
typeof(Button),
|
||||
new FrameworkPropertyMetadata(typeof(Button))
|
||||
);
|
||||
}
|
||||
|
||||
// Dependency properties
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), ...);
|
||||
}
|
||||
```
|
||||
|
||||
**XAML Style Pattern:**
|
||||
```xaml
|
||||
<!-- Controls/Button/Button.xaml -->
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Wpf.Ui.Controls">
|
||||
|
||||
<Thickness x:Key="ButtonPadding">11,5,11,6</Thickness>
|
||||
|
||||
<Style TargetType="{x:Type controls:Button}">
|
||||
<Setter Property="OverridesDefaultStyle" Value="True" />
|
||||
<Setter Property="SnapsToDevicePixels" Value="True" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type controls:Button}">
|
||||
<!-- Control template here -->
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
### Flat Namespace Strategy
|
||||
|
||||
**All controls use a single namespace:** `Wpf.Ui.Controls`
|
||||
|
||||
```csharp
|
||||
// Physical path: Controls/Button/Button.cs
|
||||
namespace Wpf.Ui.Controls; // NOT Wpf.Ui.Controls.Button
|
||||
// ReSharper disable once CheckNamespace // Suppress warning
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Simpler XAML namespace mapping (`xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"`)
|
||||
- No need for consumers to know physical folder structure
|
||||
- Consistent with WPF framework controls (all in System.Windows.Controls)
|
||||
- Better intellisense experience (all controls in one namespace dropdown)
|
||||
|
||||
**Trade-off:** IDE warning suppression required (`IDE0130: CheckNamespace`)
|
||||
|
||||
### Partial Class Decomposition
|
||||
|
||||
Complex controls split across multiple files:
|
||||
|
||||
**NavigationView example:**
|
||||
- **NavigationView.Base.cs** - Core logic, template application
|
||||
- **NavigationView.Properties.cs** - 27 dependency properties
|
||||
- **NavigationView.Events.cs** - 7 routed events
|
||||
- **NavigationView.Navigation.cs** - Navigation journal logic
|
||||
- **NavigationView.TemplateParts.cs** - Template part fields and bindings
|
||||
- **NavigationView.AttachedProperties.cs** - Attached properties
|
||||
|
||||
**ContentDialog example:**
|
||||
- **ContentDialog.cs** - Main implementation (714 lines)
|
||||
- **ContentDialog.FocusBehavior.cs** - Keyboard focus management
|
||||
|
||||
**Benefits:**
|
||||
- Files stay under 300-500 lines (typically)
|
||||
- Clear separation of concerns
|
||||
- Easy to navigate specific aspects
|
||||
- Reduces merge conflicts
|
||||
|
||||
**Naming Convention:** `{ControlName}.{Concern}.cs`
|
||||
|
||||
### DependencyProperty Pattern
|
||||
|
||||
**Registration:**
|
||||
```csharp
|
||||
/// <summary>Identifies the <see cref="Icon"/> dependency property.</summary>
|
||||
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
|
||||
nameof(Icon),
|
||||
typeof(IconElement),
|
||||
typeof(Button),
|
||||
new PropertyMetadata(null, OnIconChanged, IconElement.Coerce)
|
||||
);
|
||||
```
|
||||
|
||||
**CLR Wrapper:**
|
||||
```csharp
|
||||
[Bindable(true)]
|
||||
[Category("Appearance")]
|
||||
public IconElement? Icon
|
||||
{
|
||||
get => (IconElement?)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
```
|
||||
|
||||
**Callback Pattern:**
|
||||
```csharp
|
||||
private static void OnIconChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is Button button)
|
||||
{
|
||||
button.UpdateIconVisibility();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Capability Interfaces
|
||||
|
||||
Controls implement interfaces for cross-cutting capabilities:
|
||||
|
||||
#### IAppearanceControl
|
||||
```csharp
|
||||
public interface IAppearanceControl
|
||||
{
|
||||
ControlAppearance Appearance { get; set; }
|
||||
}
|
||||
|
||||
public enum ControlAppearance
|
||||
{
|
||||
Primary, Secondary, Info, Dark, Light,
|
||||
Danger, Success, Caution, Transparent
|
||||
}
|
||||
```
|
||||
|
||||
**Used by:** Button, Badge, Snackbar, HyperlinkButton
|
||||
|
||||
#### IIconControl
|
||||
```csharp
|
||||
public interface IIconControl
|
||||
{
|
||||
IconElement? Icon { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Used by:** Button, NavigationViewItem, AutoSuggestBox
|
||||
|
||||
#### IThemeControl
|
||||
```csharp
|
||||
public interface IThemeControl
|
||||
{
|
||||
Appearance.ApplicationTheme ApplicationTheme { get; }
|
||||
}
|
||||
```
|
||||
|
||||
**Used by:** TitleBar, controls that need direct theme awareness
|
||||
|
||||
**Benefits:**
|
||||
- Type-safe capability detection
|
||||
- Shared behavior implementation
|
||||
- Consistent property naming across controls
|
||||
|
||||
## Control Categories
|
||||
|
||||
### Window Chrome
|
||||
FluentWindow, TitleBar, ClientAreaBorder, Window
|
||||
|
||||
### Navigation
|
||||
NavigationView, NavigationViewItem, BreadcrumbBar, TabControl, TabView, Menu
|
||||
|
||||
### Buttons
|
||||
Button, HyperlinkButton, DropDownButton, SplitButton, ToggleButton, ToggleSwitch
|
||||
|
||||
### Text Input
|
||||
TextBox, PasswordBox, RichTextBox, AutoSuggestBox, NumberBox
|
||||
|
||||
### Dialogs & Overlays
|
||||
ContentDialog, ContentDialogHost, MessageBox, Flyout, Snackbar, SnackbarPresenter
|
||||
|
||||
### Data Display
|
||||
Card, InfoBar, InfoBadge, Badge, ListView, DataGrid, TreeView
|
||||
|
||||
### Pickers
|
||||
CalendarDatePicker, DatePicker, TimePicker, ColorPicker
|
||||
|
||||
### Progress & Feedback
|
||||
ProgressBar, ProgressRing, RatingControl, ThumbRate
|
||||
|
||||
### Icons
|
||||
IconElement, FontIcon, SymbolIcon, ImageIcon, IconSourceElement
|
||||
|
||||
### Layout
|
||||
Anchor, Page, Frame, Expander, Separator, Slider
|
||||
|
||||
## Enforcement
|
||||
|
||||
### MUST Follow
|
||||
|
||||
1. **Each control in its own subfolder** under `Controls/{ControlName}/`
|
||||
2. **Paired .cs + .xaml files** with matching names
|
||||
3. **Flat namespace** `Wpf.Ui.Controls` for all controls
|
||||
4. **Static constructor** with `DefaultStyleKeyProperty.OverrideMetadata`
|
||||
5. **Implicit style** in XAML with `TargetType` (no `x:Key`)
|
||||
6. **`OverridesDefaultStyle=True`** in all control styles
|
||||
7. **`SnapsToDevicePixels=True`** in all control styles
|
||||
8. **XML documentation** with `<summary>` and `<example>` for public API
|
||||
|
||||
### MUST NOT Do
|
||||
|
||||
1. **Never nest controls** in subdirectories (keep flat under Controls/)
|
||||
2. **Never use different namespace** from `Wpf.Ui.Controls`
|
||||
3. **Never create keyed styles** as primary style (use implicit TargetType)
|
||||
4. **Never reference controls** by folder structure in documentation
|
||||
|
||||
### Verification
|
||||
|
||||
- IDE0130 (CheckNamespace) suppressed in .editorconfig
|
||||
- WpfAnalyzers enforces DependencyProperty correctness
|
||||
- StyleCop rules enforce XML documentation (SA1600 suppressed for internal members)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Highly Discoverable:** Single namespace for all 77+ controls
|
||||
- **Scalable:** Adding new controls doesn't affect existing structure
|
||||
- **Maintainable:** Clear boundaries and partial class decomposition
|
||||
- **Type-Safe:** Interface-based capabilities enable polymorphism
|
||||
- **Consistent:** Uniform naming and organization patterns
|
||||
|
||||
### Negative
|
||||
- **IDE Warnings:** Namespace/folder mismatch requires suppression
|
||||
- **Large Directory:** 77+ control folders in single directory
|
||||
- **No Categorization:** Physical structure doesn't reflect logical categories
|
||||
- **Partial Class Complexity:** Large controls split across many files
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Nested Category Folders
|
||||
```
|
||||
Controls/
|
||||
├── Buttons/
|
||||
│ ├── Button/
|
||||
│ └── ToggleSwitch/
|
||||
└── Navigation/
|
||||
└── NavigationView/
|
||||
```
|
||||
|
||||
**Rejected:** Would require category-specific namespaces or deeper folder/namespace mismatch.
|
||||
|
||||
### Category-Based Namespaces
|
||||
```csharp
|
||||
namespace Wpf.Ui.Controls.Buttons;
|
||||
namespace Wpf.Ui.Controls.Navigation;
|
||||
```
|
||||
|
||||
**Rejected:** Requires consumers to know category classification. Inconsistent with WPF framework patterns.
|
||||
|
||||
### Single File Per Control
|
||||
**Rejected:** Controls like NavigationView have 1000+ lines. Unmanageable in single file.
|
||||
|
||||
## References
|
||||
- WPF Framework Control Architecture: `System.Windows.Controls` namespace
|
||||
- WinUI 3 Control Architecture: Flat namespace strategy
|
||||
- [WPF Control Authoring](https://docs.microsoft.com/dotnet/desktop/wpf/controls/control-authoring-overview)
|
||||
@@ -0,0 +1,399 @@
|
||||
# ADR-003: Win32 Interop via CsWin32
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
WPF UI requires extensive Win32 API access for features unavailable in standard WPF:
|
||||
- Desktop Window Manager (DWM) effects (Mica, Acrylic backdrops)
|
||||
- Window corner rounding (Windows 11)
|
||||
- Dark mode title bars
|
||||
- System tray icon management
|
||||
- System theme detection
|
||||
- Taskbar progress indicators
|
||||
|
||||
Traditional P/Invoke requires:
|
||||
- Manual function signature declarations
|
||||
- COM interface definitions
|
||||
- Struct layout definitions
|
||||
- Constant value definitions
|
||||
- Maintaining cross-architecture compatibility (x86/x64/ARM64)
|
||||
|
||||
## Decision
|
||||
|
||||
### Use CsWin32 Source Generator
|
||||
|
||||
**Package:** Microsoft.Windows.CsWin32 (build-time only, PrivateAssets="all")
|
||||
|
||||
**Declaration File:** `src/Wpf.Ui/NativeMethods.txt`
|
||||
|
||||
CsWin32 generates P/Invoke bindings at compile-time from Win32 metadata.
|
||||
|
||||
### NativeMethods.txt Format
|
||||
|
||||
```
|
||||
# DWM Functions
|
||||
DwmIsCompositionEnabled
|
||||
DwmSetWindowAttribute
|
||||
DwmExtendFrameIntoClientArea
|
||||
S_OK
|
||||
SetWindowThemeAttribute
|
||||
DWM_SYSTEMBACKDROP_TYPE
|
||||
DWM_WINDOW_CORNER_PREFERENCE
|
||||
DWMWA_COLOR_NONE
|
||||
WTA_OPTIONS
|
||||
|
||||
# Window Management
|
||||
GetDpiForWindow
|
||||
GetForegroundWindow
|
||||
IsWindowVisible
|
||||
SetWindowRgn
|
||||
GetWindowRect
|
||||
GetSystemMetrics
|
||||
WINDOW_STYLE
|
||||
|
||||
# COM Interfaces
|
||||
ITaskbarList4
|
||||
TaskbarList
|
||||
|
||||
# Wildcard Patterns
|
||||
WM_*
|
||||
HT*
|
||||
```
|
||||
|
||||
**CsWin32 automatically generates:**
|
||||
- Function P/Invoke declarations
|
||||
- Struct definitions with correct layout
|
||||
- Enum types
|
||||
- COM interface wrappers
|
||||
- Foundation types (HWND, HRESULT, BOOL, etc.)
|
||||
|
||||
### Generated Code Location
|
||||
**Namespace:** `Windows.Win32` and `Windows.Win32.Foundation`
|
||||
**Physical Location:** `obj/` directory (not committed to source control)
|
||||
|
||||
**Usage:**
|
||||
```csharp
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
|
||||
// Generated type-safe P/Invoke
|
||||
HRESULT result = PInvoke.DwmSetWindowAttribute(
|
||||
new HWND(windowHandle),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&darkMode,
|
||||
sizeof(BOOL)
|
||||
);
|
||||
```
|
||||
|
||||
## Three-Layer Architecture
|
||||
|
||||
### Layer 1: CsWin32 Generated Code
|
||||
**Purpose:** Auto-generated P/Invoke declarations
|
||||
|
||||
**Characteristics:**
|
||||
- Compile-time generated
|
||||
- Type-safe API surface
|
||||
- Cross-architecture compatible
|
||||
- Not committed to source control
|
||||
|
||||
### Layer 2: Managed Wrappers
|
||||
**Purpose:** Safe, validated native API access
|
||||
|
||||
**Location:** `src/Wpf.Ui/Interop/`
|
||||
|
||||
#### UnsafeNativeMethods.cs
|
||||
```csharp
|
||||
internal static class UnsafeNativeMethods
|
||||
{
|
||||
public static unsafe bool ApplyWindowCornerPreference(
|
||||
IntPtr handle,
|
||||
WindowCornerPreference cornerPreference)
|
||||
{
|
||||
// Validation layer
|
||||
if (handle == IntPtr.Zero)
|
||||
return false;
|
||||
|
||||
if (!PInvoke.IsWindow(new HWND(handle)))
|
||||
return false;
|
||||
|
||||
// Type conversion
|
||||
DWM_WINDOW_CORNER_PREFERENCE pvAttribute =
|
||||
UnsafeReflection.Cast(cornerPreference);
|
||||
|
||||
// Native call with exception handling
|
||||
try
|
||||
{
|
||||
HRESULT hr = PInvoke.DwmSetWindowAttribute(
|
||||
new HWND(handle),
|
||||
DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
&pvAttribute,
|
||||
(uint)sizeof(DWM_WINDOW_CORNER_PREFERENCE)
|
||||
);
|
||||
|
||||
return hr == HRESULT.S_OK;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Graceful degradation for unsupported OS versions
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities:**
|
||||
- Handle validation (IntPtr.Zero, IsWindow checks)
|
||||
- Exception suppression for cross-version compatibility
|
||||
- HRESULT → bool conversion
|
||||
- Type-safe enum conversions
|
||||
|
||||
#### Custom PInvoke.cs
|
||||
**Purpose:** Supplement CsWin32 for missing/incorrect signatures
|
||||
|
||||
```csharp
|
||||
namespace Windows.Win32;
|
||||
|
||||
internal static partial class PInvoke
|
||||
{
|
||||
// CsWin32 doesn't generate correct SetWindowLongPtr for 32/64-bit
|
||||
[DllImport("USER32.dll",
|
||||
ExactSpelling = true,
|
||||
EntryPoint = "SetWindowLongPtrW",
|
||||
SetLastError = true)]
|
||||
internal static extern nint SetWindowLongPtr(
|
||||
HWND hWnd,
|
||||
WINDOW_LONG_PTR_INDEX nIndex,
|
||||
nint dwNewLong
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 3: High-Level Utilities
|
||||
**Purpose:** Business logic and feature implementation
|
||||
|
||||
**Locations:**
|
||||
- `src/Wpf.Ui/Win32/Utilities.cs` - OS version detection
|
||||
- `src/Wpf.Ui/Appearance/` - Theme managers
|
||||
- `src/Wpf.Ui/Controls/FluentWindow/` - Window chrome
|
||||
- `src/Wpf.Ui/Tray/` - System tray management
|
||||
|
||||
**Characteristics:**
|
||||
- Consumes Layer 2 safe wrappers
|
||||
- OS version feature gating
|
||||
- Business logic and state management
|
||||
|
||||
## Handle Validation Pattern
|
||||
|
||||
**Critical Requirement:** All native calls MUST validate handles.
|
||||
|
||||
```csharp
|
||||
public static bool NativeOperation(IntPtr handle)
|
||||
{
|
||||
// Step 1: Null check
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Verify window exists
|
||||
if (!PInvoke.IsWindow(new HWND(handle)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 3: Perform operation
|
||||
HRESULT hr = PInvoke.SomeWin32Function(new HWND(handle), ...);
|
||||
return hr == HRESULT.S_OK;
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Handles can become invalid between retrieval and use
|
||||
- Window may be destroyed on background thread
|
||||
- Invalid handles cause native crashes
|
||||
- IsWindow is inexpensive (single User32 call)
|
||||
|
||||
## Exception Handling Strategy
|
||||
|
||||
**Philosophy:** Native APIs fail silently across Windows versions. Prefer graceful degradation over exceptions.
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
HRESULT hr = PInvoke.DwmSetWindowAttribute(...);
|
||||
return hr == HRESULT.S_OK;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
// API not available on this Windows version
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Unexpected failure, degrade gracefully
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
**Suppressed Exceptions:**
|
||||
- `COMException` - COM API failures
|
||||
- `Win32Exception` - Native API errors
|
||||
- `EntryPointNotFoundException` - API not available on OS version
|
||||
- `DllNotFoundException` - DLL not present
|
||||
|
||||
## Conditional Compilation
|
||||
|
||||
### Framework-Specific Code
|
||||
```csharp
|
||||
#if NET5_0_OR_GREATER
|
||||
// Modern API available
|
||||
var version = Environment.OSVersion;
|
||||
#else
|
||||
// Fallback for .NET Framework
|
||||
var version = GetVersionFromRegistry();
|
||||
#endif
|
||||
```
|
||||
|
||||
### OS Version Feature Gating
|
||||
```csharp
|
||||
// Windows 11+ only features
|
||||
if (Win32.Utilities.IsOSWindows11OrNewer)
|
||||
{
|
||||
UnsafeNativeMethods.ApplyWindowCornerPreference(
|
||||
handle,
|
||||
WindowCornerPreference.Round
|
||||
);
|
||||
}
|
||||
|
||||
// DWM composition required
|
||||
if (Win32.Utilities.IsCompositionEnabled)
|
||||
{
|
||||
UnsafeNativeMethods.ApplyWindowBackdrop(
|
||||
handle,
|
||||
WindowBackdropType.Acrylic
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Enforcement
|
||||
|
||||
### MUST Follow
|
||||
|
||||
1. **Add new Win32 APIs to NativeMethods.txt** (never manual P/Invoke unless CsWin32 fails)
|
||||
2. **Validate handles** before all native calls (IntPtr.Zero + IsWindow)
|
||||
3. **Return bool** from wrapper methods indicating success
|
||||
4. **Suppress exceptions** in interop layer for compatibility
|
||||
5. **Use unsafe keyword** explicitly for pointer operations
|
||||
6. **Feature-gate by OS version** for version-specific APIs
|
||||
7. **Use HRESULT == S_OK** pattern for success checking
|
||||
8. **Keep generated code private** (internal/private visibility)
|
||||
|
||||
### MUST NOT Do
|
||||
|
||||
1. **Never call PInvoke directly** from high-level code (use UnsafeNativeMethods wrappers)
|
||||
2. **Never skip handle validation** (even if "guaranteed" valid)
|
||||
3. **Never throw exceptions** from interop wrappers (return false instead)
|
||||
4. **Never assume API availability** across Windows versions
|
||||
5. **Never use var** for native types (HRESULT, HWND, etc. - explicit types required)
|
||||
6. **Never commit obj/ directory** (contains generated code)
|
||||
|
||||
### Verification
|
||||
|
||||
- WpfAnalyzers enforces correct patterns
|
||||
- Code review checks handle validation
|
||||
- Multi-version testing (Windows 7, 8.1, 10, 11)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Type Safety:** CsWin32 generates correct signatures from metadata
|
||||
- **Maintenance:** Win32 metadata updates automatically benefit project
|
||||
- **Cross-Platform:** ARM64, x64, x86 handled automatically
|
||||
- **Correctness:** Struct layouts, calling conventions verified by Microsoft
|
||||
- **Discoverability:** IntelliSense for all Windows APIs
|
||||
- **Build-Time Only:** Zero runtime dependencies
|
||||
|
||||
### Negative
|
||||
- **Build-Time Dependency:** Requires CsWin32 NuGet package
|
||||
- **Opaque Generation:** Generated code in obj/, harder to debug
|
||||
- **NativeMethods.txt Maintenance:** Must manually add new APIs (currently 34 lines, including wildcard patterns like `WM_*` and `HT*`)
|
||||
- **Supplements Needed:** Some APIs require manual P/Invoke (SetWindowLongPtr)
|
||||
- **Learning Curve:** Developers must understand NativeMethods.txt format
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### CsWin32 Performance
|
||||
- **Zero overhead:** Generated code identical to hand-written P/Invoke
|
||||
- **Inlined by JIT:** Same JIT optimization as manual declarations
|
||||
- **No reflection:** Compile-time code generation
|
||||
|
||||
### Handle Validation Cost
|
||||
- **IsWindow:** Single User32 API call (~1-2 μs)
|
||||
- **Negligible:** Compared to DWM/window operations (hundreds of μs)
|
||||
- **Essential:** Prevents native crashes worth the cost
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Manual P/Invoke
|
||||
**Rejected:**
|
||||
- High maintenance burden (200+ Win32 functions across project)
|
||||
- Error-prone struct layout definitions
|
||||
- Cross-architecture compatibility issues
|
||||
- No automatic updates from Windows SDK
|
||||
|
||||
### ComWrappers
|
||||
**Rejected:**
|
||||
- Only solves COM interop, not general P/Invoke
|
||||
- More complex than CsWin32
|
||||
- Limited to .NET 5+
|
||||
|
||||
### PInvoke.net Snippets
|
||||
**Rejected:**
|
||||
- Community-maintained, not authoritative
|
||||
- Copy-paste errors common
|
||||
- No compile-time verification
|
||||
- Inconsistent signature styles
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Adding New Win32 API
|
||||
|
||||
1. **Add to NativeMethods.txt:**
|
||||
```
|
||||
DwmGetColorizationColor
|
||||
```
|
||||
|
||||
2. **Rebuild project** (CsWin32 generates code)
|
||||
|
||||
3. **Create managed wrapper in UnsafeNativeMethods.cs:**
|
||||
```csharp
|
||||
public static bool GetColorizationColor(out Color color)
|
||||
{
|
||||
try
|
||||
{
|
||||
HRESULT hr = PInvoke.DwmGetColorizationColor(out uint colorValue, out BOOL opaque);
|
||||
color = Color.FromArgb(...);
|
||||
return hr == HRESULT.S_OK;
|
||||
}
|
||||
catch
|
||||
{
|
||||
color = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Consume from high-level code:**
|
||||
```csharp
|
||||
if (UnsafeNativeMethods.GetColorizationColor(out Color color))
|
||||
{
|
||||
// Use color
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
- [CsWin32 GitHub](https://github.com/microsoft/CsWin32)
|
||||
- [Windows API Documentation](https://docs.microsoft.com/windows/win32/api/)
|
||||
- [P/Invoke Best Practices](https://docs.microsoft.com/dotnet/standard/native-interop/best-practices)
|
||||
@@ -0,0 +1,455 @@
|
||||
# ADR-004: Static Managers for Theming
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The theming system requires global coordination across:
|
||||
- Application resource dictionary management
|
||||
- System theme synchronization
|
||||
- Accent color application
|
||||
- Window appearance updates
|
||||
|
||||
Multiple approaches exist:
|
||||
1. **Static classes** (global singleton)
|
||||
2. **Instance-based services** (registered in DI container)
|
||||
3. **Ambient context pattern** (ThreadStatic/AsyncLocal)
|
||||
|
||||
## Decision
|
||||
|
||||
Use **static class singleton pattern** for core theme managers:
|
||||
- `ApplicationThemeManager`
|
||||
- `ApplicationAccentColorManager`
|
||||
- `SystemThemeWatcher`
|
||||
- `WindowBackgroundManager`
|
||||
- `ResourceDictionaryManager` (internal)
|
||||
|
||||
### Implementation Pattern
|
||||
|
||||
```csharp
|
||||
public static class ApplicationThemeManager
|
||||
{
|
||||
// Global state
|
||||
private static ApplicationTheme _currentTheme = ApplicationTheme.Unknown;
|
||||
|
||||
// Global event
|
||||
public static event ThemeChangedEvent? Changed;
|
||||
|
||||
// Static methods
|
||||
public static void Apply(ApplicationTheme theme)
|
||||
{
|
||||
if (_currentTheme == theme)
|
||||
return;
|
||||
|
||||
ResourceDictionaryManager manager = new(LibraryNamespace);
|
||||
manager.UpdateDictionary("theme", GetThemeUri(theme));
|
||||
|
||||
_currentTheme = theme;
|
||||
Changed?.Invoke(theme, GetSystemAccent());
|
||||
}
|
||||
|
||||
public static ApplicationTheme GetAppTheme()
|
||||
{
|
||||
return _currentTheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### No Constructor, No Instances
|
||||
```csharp
|
||||
public static class ApplicationThemeManager
|
||||
{
|
||||
// All members are static
|
||||
// Cannot be instantiated
|
||||
// Cannot be inherited
|
||||
// Cannot be mocked/substituted
|
||||
}
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### Simple API Surface
|
||||
```csharp
|
||||
// Immediate clarity of global scope
|
||||
ApplicationThemeManager.Apply(ApplicationTheme.Dark);
|
||||
var current = ApplicationThemeManager.GetAppTheme();
|
||||
```
|
||||
|
||||
Compared to instance-based:
|
||||
```csharp
|
||||
// Requires context about where themeManager comes from
|
||||
_themeManager.Apply(ApplicationTheme.Dark);
|
||||
var current = _themeManager.GetAppTheme();
|
||||
```
|
||||
|
||||
### Single Source of Truth
|
||||
Application theme is fundamentally global state:
|
||||
- Only one theme can be active
|
||||
- All windows share the same theme
|
||||
- Resource dictionaries are process-wide
|
||||
|
||||
Static class enforces singleton semantics at compile-time.
|
||||
|
||||
### No DI Configuration Required
|
||||
```csharp
|
||||
// Works immediately without setup
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplicationThemeManager.Apply(ApplicationTheme.Dark);
|
||||
}
|
||||
```
|
||||
|
||||
Instance-based would require:
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
services.AddSingleton<IThemeManager, ThemeManager>();
|
||||
|
||||
// MainWindow.xaml.cs
|
||||
public MainWindow(IThemeManager themeManager)
|
||||
{
|
||||
_themeManager = themeManager;
|
||||
InitializeComponent();
|
||||
_themeManager.Apply(ApplicationTheme.Dark);
|
||||
}
|
||||
```
|
||||
|
||||
### WPF Application Model Alignment
|
||||
WPF itself uses static patterns extensively:
|
||||
- `Application.Current` (static property)
|
||||
- `Application.Current.Resources` (global resource dictionary)
|
||||
- `SystemColors` (static class)
|
||||
- `SystemParameters` (static class)
|
||||
|
||||
## Trade-offs
|
||||
|
||||
### Advantages
|
||||
|
||||
**1. Simplicity**
|
||||
- No DI configuration required
|
||||
- No interface abstraction needed
|
||||
- Clear that state is global
|
||||
- Immediate usability
|
||||
|
||||
**2. Performance**
|
||||
- Zero overhead (no interface dispatch)
|
||||
- No allocation for manager instances
|
||||
- Direct static method calls
|
||||
|
||||
**3. Discoverability**
|
||||
- Easy to find with IntelliSense
|
||||
- Self-documenting via naming (`ApplicationThemeManager` clearly global)
|
||||
- No need to understand DI container
|
||||
|
||||
**4. Compatibility**
|
||||
- Works in non-DI scenarios (simple WPF apps)
|
||||
- Compatible with .NET Framework patterns
|
||||
- No breaking changes if DI added later
|
||||
|
||||
### Disadvantages
|
||||
|
||||
**1. Limited Testability**
|
||||
- Cannot mock static classes
|
||||
- Difficult to isolate in unit tests
|
||||
- Tests may affect each other through shared state
|
||||
|
||||
**Mitigation:**
|
||||
- Extract `IThemeService` for consumers who need testability
|
||||
- Test through integration tests instead of unit tests
|
||||
- Use `[Collection]` attribute in XUnit to isolate test state
|
||||
|
||||
**2. Hidden Dependencies**
|
||||
- Static call hides dependency
|
||||
- Harder to track theme manager usage
|
||||
- Violates dependency injection principle
|
||||
|
||||
**Mitigation:**
|
||||
- Theme management is intentionally global
|
||||
- Not a "hidden" dependency if explicitly documented as global
|
||||
|
||||
**3. Global State**
|
||||
- Mutable global state
|
||||
- Concurrent access concerns
|
||||
- No lifetime management
|
||||
|
||||
**Mitigation:**
|
||||
- Theme changes are inherently single-threaded (UI thread)
|
||||
- `Application.Current.Resources` is already global mutable state
|
||||
- No need for lifetime management (lives entire process lifetime)
|
||||
|
||||
**4. No Polymorphism**
|
||||
- Cannot substitute alternative implementations
|
||||
- Cannot extend behavior through inheritance
|
||||
|
||||
**Mitigation:**
|
||||
- Theme system is not extensible by design
|
||||
- Alternative implementations not a use case
|
||||
|
||||
## Service Interface for DI
|
||||
|
||||
For consumers requiring testability, `IThemeService` wraps static managers:
|
||||
|
||||
```csharp
|
||||
public interface IThemeService
|
||||
{
|
||||
ApplicationTheme GetTheme();
|
||||
SystemTheme GetNativeSystemTheme();
|
||||
ApplicationTheme GetSystemTheme();
|
||||
bool SetTheme(ApplicationTheme applicationTheme);
|
||||
bool SetSystemAccent();
|
||||
bool SetAccent(Color accentColor);
|
||||
bool SetAccent(SolidColorBrush accentSolidBrush);
|
||||
}
|
||||
|
||||
public partial class ThemeService : IThemeService
|
||||
{
|
||||
public ApplicationTheme GetTheme()
|
||||
=> ApplicationThemeManager.GetAppTheme();
|
||||
|
||||
public SystemTheme GetNativeSystemTheme()
|
||||
=> ApplicationThemeManager.GetSystemTheme();
|
||||
|
||||
public ApplicationTheme GetSystemTheme()
|
||||
=> ApplicationThemeManager.GetSystemTheme() switch { ... };
|
||||
|
||||
public bool SetTheme(ApplicationTheme applicationTheme)
|
||||
{
|
||||
ApplicationThemeManager.Apply(applicationTheme);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetSystemAccent()
|
||||
{
|
||||
ApplicationAccentColorManager.ApplySystemAccent();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetAccent(Color accentColor)
|
||||
{
|
||||
ApplicationAccentColorManager.Apply(accentColor);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetAccent(SolidColorBrush accentSolidBrush)
|
||||
{
|
||||
ApplicationAccentColorManager.Apply(accentSolidBrush.Color);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Registration:**
|
||||
```csharp
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
```
|
||||
|
||||
**Usage in testable code:**
|
||||
```csharp
|
||||
public class SettingsViewModel
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
|
||||
public SettingsViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
}
|
||||
|
||||
public void ApplyDarkMode()
|
||||
{
|
||||
_themeService.SetTheme(ApplicationTheme.Dark);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Testing with mock:**
|
||||
```csharp
|
||||
[Fact]
|
||||
public void ApplyDarkMode_CallsThemeService()
|
||||
{
|
||||
// Arrange
|
||||
var mockThemeService = Substitute.For<IThemeService>();
|
||||
var viewModel = new SettingsViewModel(mockThemeService);
|
||||
|
||||
// Act
|
||||
viewModel.ApplyDarkMode();
|
||||
|
||||
// Assert
|
||||
mockThemeService.Received(1).SetTheme(ApplicationTheme.Dark);
|
||||
}
|
||||
```
|
||||
|
||||
## UiApplication Pattern
|
||||
|
||||
`UiApplication` uses `[ThreadStatic]` for thread-local singleton:
|
||||
|
||||
```csharp
|
||||
public class UiApplication
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static UiApplication? _uiApplication;
|
||||
|
||||
public static UiApplication? Current => _uiApplication;
|
||||
|
||||
public UiApplication(Application application)
|
||||
{
|
||||
// Stores the application reference and sets _uiApplication
|
||||
}
|
||||
|
||||
// Instance methods operate on the wrapped Application
|
||||
public ResourceDictionary Resources => Application.Current.Resources;
|
||||
}
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- Non-static class instantiated with an `Application` parameter
|
||||
- Uses `[ThreadStatic]` backing field `_uiApplication` for thread-local singleton
|
||||
- Each UI thread gets its own `UiApplication` instance
|
||||
- Safely wraps `Application.Current` (which is also thread-local)
|
||||
- Allows future expansion with per-thread state
|
||||
|
||||
## Enforcement
|
||||
|
||||
### MUST Follow
|
||||
|
||||
1. **Use static managers directly** for simple scenarios:
|
||||
```csharp
|
||||
ApplicationThemeManager.Apply(ApplicationTheme.Dark);
|
||||
```
|
||||
|
||||
2. **Use IThemeService** in testable/DI-dependent code:
|
||||
```csharp
|
||||
public ViewModel(IThemeService themeService) { }
|
||||
```
|
||||
|
||||
3. **Document global nature** in XML docs:
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Global theme manager. Applies themes application-wide.
|
||||
/// </summary>
|
||||
```
|
||||
|
||||
4. **Never create wrapper instances** of static managers:
|
||||
```csharp
|
||||
// BAD: Don't do this
|
||||
public class ThemeManagerWrapper
|
||||
{
|
||||
private ApplicationTheme _cachedTheme;
|
||||
|
||||
public void Apply(ApplicationTheme theme)
|
||||
{
|
||||
_cachedTheme = theme;
|
||||
ApplicationThemeManager.Apply(theme);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MUST NOT Do
|
||||
|
||||
1. **Never attempt to mock static classes** in unit tests
|
||||
- Use `IThemeService` instead if testing is needed
|
||||
|
||||
2. **Never cache theme state** in instance fields
|
||||
- Always query `ApplicationThemeManager.GetAppTheme()` for current state
|
||||
|
||||
3. **Never create parallel theme systems**
|
||||
- Static managers are the single source of truth
|
||||
|
||||
### Verification
|
||||
|
||||
- Code review enforces proper usage
|
||||
- Documentation clearly marks classes as static global managers
|
||||
- `IThemeService` provides tested alternative where needed
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Integration Tests
|
||||
Theme system is tested through integration tests that exercise full stack:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ThemeChange_UpdatesWindowAppearance()
|
||||
{
|
||||
// Arrange
|
||||
var window = new FluentWindow();
|
||||
window.Show();
|
||||
|
||||
// Act
|
||||
ApplicationThemeManager.Apply(ApplicationTheme.Dark);
|
||||
await Task.Delay(500); // Allow visual update
|
||||
|
||||
// Assert
|
||||
var theme = ApplicationThemeManager.GetAppTheme();
|
||||
Assert.Equal(ApplicationTheme.Dark, theme);
|
||||
|
||||
// Cleanup
|
||||
window.Close();
|
||||
}
|
||||
```
|
||||
|
||||
### Unit Tests for Consumer Code
|
||||
Consumer code uses `IThemeService` for testability:
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void ApplyDarkTheme_UpdatesCurrentTheme()
|
||||
{
|
||||
var themeService = Substitute.For<IThemeService>();
|
||||
var viewModel = new SettingsViewModel(themeService);
|
||||
|
||||
viewModel.ApplyDarkTheme();
|
||||
|
||||
themeService.Received().SetTheme(ApplicationTheme.Dark);
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
All static manager classes include XML doc warning:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Global static manager for application theming.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is a static class managing process-wide theme state.
|
||||
/// Theme changes affect all windows in the application.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For testable code, use <see cref="IThemeService"/> instead.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class ApplicationThemeManager
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Migration to Instances
|
||||
If future requirements demand it, can migrate while maintaining compatibility:
|
||||
|
||||
```csharp
|
||||
// New instance-based implementation
|
||||
public sealed class ThemeManager : IThemeManager
|
||||
{
|
||||
// Instance implementation
|
||||
}
|
||||
|
||||
// Static facade maintains compatibility
|
||||
public static class ApplicationThemeManager
|
||||
{
|
||||
private static readonly IThemeManager _instance = new ThemeManager();
|
||||
|
||||
public static void Apply(ApplicationTheme theme)
|
||||
=> _instance.Apply(theme);
|
||||
}
|
||||
```
|
||||
|
||||
This preserves existing code while enabling DI-based usage.
|
||||
|
||||
## References
|
||||
- WPF Static Classes: `Application.Current`, `SystemColors`, `SystemParameters`
|
||||
- Gang of Four Singleton Pattern
|
||||
- [Dependency Injection Anti-Pattern: Service Locator](https://blog.ploeh.dk/2010/02/03/ServiceLocatorisanAnti-Pattern/)
|
||||
@@ -0,0 +1,395 @@
|
||||
# ADR-005: Feature Folder Organization for Controls
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
With 77+ controls in the library, code organization becomes critical for maintainability. The structure must support:
|
||||
- Clear isolation between controls
|
||||
- Easy location of control-related files
|
||||
- Logical grouping of complex control components
|
||||
- Scalability as new controls are added
|
||||
|
||||
## Decision
|
||||
|
||||
### Feature Folder per Control
|
||||
|
||||
Each control resides in `Controls/{ControlName}/` directory:
|
||||
|
||||
```
|
||||
Controls/
|
||||
├── Button/
|
||||
│ ├── Button.cs
|
||||
│ └── Button.xaml
|
||||
├── Card/
|
||||
│ ├── Card.cs
|
||||
│ └── Card.xaml
|
||||
├── NavigationView/
|
||||
│ ├── NavigationView.Base.cs
|
||||
│ ├── NavigationView.Properties.cs
|
||||
│ ├── NavigationView.Events.cs
|
||||
│ ├── NavigationView.Navigation.cs
|
||||
│ ├── NavigationView.TemplateParts.cs
|
||||
│ ├── NavigationView.AttachedProperties.cs
|
||||
│ ├── NavigationView.xaml
|
||||
│ ├── NavigationViewItem.cs
|
||||
│ ├── NavigationViewItemHeader.cs
|
||||
│ └── NavigationViewItemSeparator.cs
|
||||
```
|
||||
|
||||
**One control = one folder** with all related files.
|
||||
|
||||
## Structure Patterns
|
||||
|
||||
### Simple Controls
|
||||
**Pattern:** Single .cs + .xaml pair
|
||||
|
||||
```
|
||||
Badge/
|
||||
├── Badge.cs # Control implementation
|
||||
└── Badge.xaml # Implicit style
|
||||
```
|
||||
|
||||
**Applies to:** Button, Badge, Card, InfoBar, TextBox, etc. (50+ controls)
|
||||
|
||||
### Complex Controls with Partial Classes
|
||||
**Pattern:** Multiple partial class files organized by concern
|
||||
|
||||
```
|
||||
NavigationView/
|
||||
├── NavigationView.Base.cs # Core control logic
|
||||
├── NavigationView.Properties.cs # 27 dependency properties
|
||||
├── NavigationView.Events.cs # 7 routed events
|
||||
├── NavigationView.Navigation.cs # Page navigation logic
|
||||
├── NavigationView.TemplateParts.cs # Template part fields/binding
|
||||
├── NavigationView.AttachedProperties.cs # Attached property definitions
|
||||
└── NavigationView.xaml # Implicit style + template
|
||||
```
|
||||
|
||||
**Partial class naming:** `{ControlName}.{Concern}.cs`
|
||||
|
||||
**Applies to:** NavigationView, ContentDialog, TitleBar
|
||||
|
||||
### Controls with Related Types
|
||||
**Pattern:** Additional related controls in same folder
|
||||
|
||||
```
|
||||
NavigationView/
|
||||
├── NavigationView*.cs # Main control (6 files)
|
||||
├── NavigationView.xaml
|
||||
├── NavigationViewItem.cs # Item container
|
||||
├── NavigationViewItemHeader.cs # Header item
|
||||
├── NavigationViewItemSeparator.cs # Separator
|
||||
├── NavigationViewContentPresenter.cs # Content host
|
||||
├── INavigationView.cs # Interface
|
||||
└── INavigationViewItem.cs # Item interface
|
||||
```
|
||||
|
||||
**Rationale:** Tightly coupled types that are only used together.
|
||||
|
||||
### Controls with Supporting Subdirectories
|
||||
**Pattern:** Subfolder for supporting types
|
||||
|
||||
```
|
||||
ContentDialog/
|
||||
├── ContentDialog.cs
|
||||
├── ContentDialog.FocusBehavior.cs
|
||||
├── ContentDialog.xaml
|
||||
├── ContentDialogHost.cs
|
||||
├── ContentDialogHostBehavior.cs
|
||||
└── EventArgs/
|
||||
├── ContentDialogButtonClickEventArgs.cs
|
||||
├── ContentDialogClosingEventArgs.cs
|
||||
└── ContentDialogClosedEventArgs.cs
|
||||
```
|
||||
|
||||
**When to use subfolder:**
|
||||
- 3+ related types (EventArgs, Converters, Enums)
|
||||
- Clear sub-component (e.g., EventArgs)
|
||||
|
||||
## Flat Namespace Strategy
|
||||
|
||||
**All controls use:** `Wpf.Ui.Controls` namespace (regardless of folder depth)
|
||||
|
||||
```csharp
|
||||
// File: Controls/NavigationView/NavigationView.cs
|
||||
namespace Wpf.Ui.Controls; // Flat namespace, not Wpf.Ui.Controls.NavigationView
|
||||
// ReSharper disable once CheckNamespace
|
||||
```
|
||||
|
||||
**Trade-off:** Folder structure does not match namespace.
|
||||
|
||||
**IDE Configuration Required:**
|
||||
```ini
|
||||
# .editorconfig
|
||||
dotnet_diagnostic.IDE0130.severity = none # Suppress namespace/folder mismatch
|
||||
```
|
||||
|
||||
## Partial Class Decomposition Strategies
|
||||
|
||||
### By Concern
|
||||
NavigationView splits by logical concern:
|
||||
- **Base.cs** - Control infrastructure (template application, initialization)
|
||||
- **Properties.cs** - Dependency property definitions
|
||||
- **Events.cs** - Routed event definitions
|
||||
- **Navigation.cs** - Page navigation logic
|
||||
- **TemplateParts.cs** - Template part fields and OnApplyTemplate logic
|
||||
- **AttachedProperties.cs** - Attached property definitions
|
||||
|
||||
### By Feature
|
||||
ContentDialog splits by feature:
|
||||
- **ContentDialog.cs** - Main implementation (async ShowAsync, dialog lifecycle)
|
||||
- **ContentDialog.FocusBehavior.cs** - Keyboard focus management (isolated behavior)
|
||||
|
||||
### Guidelines for Splitting
|
||||
|
||||
**When to split:**
|
||||
- File exceeds 500 lines
|
||||
- Clear separation of concerns exists (properties vs. logic)
|
||||
- Feature is self-contained and isolatable
|
||||
|
||||
**How to name:**
|
||||
- **Base.cs** - Core control logic (template, initialization)
|
||||
- **Properties.cs** - All dependency properties
|
||||
- **Events.cs** - All routed events
|
||||
- **{Feature}.cs** - Specific feature (FocusBehavior, Animation, etc.)
|
||||
|
||||
**Don't split:**
|
||||
- Controls under 300 lines
|
||||
- No clear separation of concerns
|
||||
- When references would create circular dependencies
|
||||
|
||||
## File Naming Conventions
|
||||
|
||||
### Control Classes
|
||||
```
|
||||
{ControlName}.cs # Simple control
|
||||
{ControlName}.{Concern}.cs # Partial class with concern
|
||||
```
|
||||
|
||||
### XAML Styles
|
||||
```
|
||||
{ControlName}.xaml # Implicit style ResourceDictionary
|
||||
```
|
||||
|
||||
### Supporting Types
|
||||
```
|
||||
{TypePurpose}{ControlName}.cs # e.g., NavigationViewItem
|
||||
{ControlName}{Purpose}.cs # e.g., ContentDialogHost
|
||||
I{ControlName}.cs # Interface
|
||||
```
|
||||
|
||||
### Event Arguments
|
||||
```
|
||||
{ControlName}{EventName}EventArgs.cs
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `ContentDialogButtonClickEventArgs.cs`
|
||||
- `NavigatedEventArgs.cs`
|
||||
|
||||
## Enforcement
|
||||
|
||||
### MUST Follow
|
||||
|
||||
1. **One control = one folder** under `Controls/`
|
||||
2. **Paired .cs + .xaml** with matching names
|
||||
3. **Flat namespace** `Wpf.Ui.Controls` for all controls
|
||||
4. **Partial class naming** `{ControlName}.{Concern}.cs`
|
||||
5. **ReSharper suppress comment** when namespace doesn't match folder:
|
||||
```csharp
|
||||
namespace Wpf.Ui.Controls;
|
||||
// ReSharper disable once CheckNamespace
|
||||
```
|
||||
|
||||
6. **Keep related types together** in same folder when tightly coupled
|
||||
|
||||
### MUST NOT Do
|
||||
|
||||
1. **Never nest control folders** (keep flat under Controls/)
|
||||
```
|
||||
❌ Controls/Buttons/Button/
|
||||
✅ Controls/Button/
|
||||
```
|
||||
|
||||
2. **Never use category-specific namespace**
|
||||
```csharp
|
||||
❌ namespace Wpf.Ui.Controls.Buttons;
|
||||
✅ namespace Wpf.Ui.Controls;
|
||||
```
|
||||
|
||||
3. **Never split files arbitrarily** (must have clear separation of concerns)
|
||||
|
||||
4. **Never create more than 2 directory levels** under Controls/
|
||||
```
|
||||
✅ Controls/ContentDialog/EventArgs/
|
||||
❌ Controls/ContentDialog/EventArgs/Closing/
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
1. **IDE0130 suppressed** in .editorconfig for namespace/folder mismatch
|
||||
2. **Code review** checks folder organization
|
||||
3. **Naming conventions** enforced during PR review
|
||||
|
||||
## Benefits
|
||||
|
||||
### Developer Experience
|
||||
|
||||
**Easy to Find:**
|
||||
```bash
|
||||
# Looking for Button control?
|
||||
Controls/Button/Button.cs # Immediately obvious location
|
||||
```
|
||||
|
||||
**Clear Boundaries:**
|
||||
- All Button-related code in `Controls/Button/`
|
||||
- No cross-control dependencies (enforced by folder isolation)
|
||||
|
||||
**Scalable:**
|
||||
- Adding new control = create new folder
|
||||
- Doesn't affect existing controls
|
||||
|
||||
### Maintainability
|
||||
|
||||
**Isolation:**
|
||||
- Changes to Button don't affect Card
|
||||
- Merge conflicts localized to single control
|
||||
|
||||
**Discoverability:**
|
||||
- New developers find controls by folder name
|
||||
- Related types co-located (NavigationViewItem with NavigationView)
|
||||
|
||||
**Refactoring:**
|
||||
- Easy to split large files (add `.Properties.cs`)
|
||||
- Clear when to split (file size, concern separation)
|
||||
|
||||
### Build Performance
|
||||
|
||||
**Partial Classes:**
|
||||
- Compiler can parallelize partial class compilation
|
||||
- Changes to Properties.cs don't trigger recompilation of Navigation.cs
|
||||
|
||||
## Trade-offs
|
||||
|
||||
### Advantages
|
||||
- ✅ Clear physical organization
|
||||
- ✅ Easy to locate control files
|
||||
- ✅ Enforces encapsulation (hard to reference across control folders)
|
||||
- ✅ Scales to 100+ controls
|
||||
- ✅ Supports complex controls with many files
|
||||
|
||||
### Disadvantages
|
||||
- ❌ Folder/namespace mismatch requires suppression
|
||||
- ❌ 77+ folders in single directory (large directory)
|
||||
- ❌ No physical categorization (all controls appear equal)
|
||||
- ❌ Related controls may be far apart alphabetically (Button vs. ToggleButton)
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Category-Based Folders
|
||||
```
|
||||
Controls/
|
||||
├── Buttons/
|
||||
│ ├── Button/
|
||||
│ └── ToggleButton/
|
||||
└── Navigation/
|
||||
└── NavigationView/
|
||||
```
|
||||
|
||||
**Rejected:**
|
||||
- Requires category-specific namespaces or deeper mismatch
|
||||
- Category classification is subjective (is ToggleSwitch a Button or Input?)
|
||||
- Doesn't align with flat namespace strategy
|
||||
|
||||
### Single File Per Control
|
||||
```
|
||||
Controls/
|
||||
├── Button.cs
|
||||
├── Button.xaml
|
||||
├── NavigationView.cs
|
||||
└── NavigationView.xaml
|
||||
```
|
||||
|
||||
**Rejected:**
|
||||
- Complex controls (NavigationView 1000+ lines) unmanageable
|
||||
- Supporting types have unclear location
|
||||
- 150+ files in single directory
|
||||
|
||||
### Hybrid Categorization
|
||||
```
|
||||
Controls/
|
||||
├── Button/
|
||||
├── Input/
|
||||
│ ├── TextBox/
|
||||
│ └── NumberBox/
|
||||
└── Navigation/
|
||||
└── NavigationView/
|
||||
```
|
||||
|
||||
**Rejected:**
|
||||
- Inconsistent structure (some categories, some not)
|
||||
- Unclear where new controls go
|
||||
- Complicates namespace strategy
|
||||
|
||||
## Migration Path
|
||||
|
||||
### Adding New Simple Control
|
||||
|
||||
1. Create folder `Controls/{NewControl}/`
|
||||
2. Add `{NewControl}.cs` with control class
|
||||
3. Add `{NewControl}.xaml` with implicit style
|
||||
4. Add ReSharper suppress comment to .cs file
|
||||
|
||||
### Splitting Existing Control
|
||||
|
||||
Example: Card becomes too large
|
||||
|
||||
**Before:**
|
||||
```
|
||||
Card/
|
||||
├── Card.cs (500 lines)
|
||||
└── Card.xaml
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
Card/
|
||||
├── Card.Base.cs (200 lines - core logic)
|
||||
├── Card.Properties.cs (100 lines - dependency properties)
|
||||
├── Card.Animation.cs (100 lines - animation logic)
|
||||
└── Card.xaml
|
||||
```
|
||||
|
||||
**Refactoring steps:**
|
||||
1. Extract dependency properties to `Card.Properties.cs`
|
||||
2. Extract animation logic to `Card.Animation.cs`
|
||||
3. Keep core logic in `Card.Base.cs`
|
||||
4. All files use `partial class Card`
|
||||
|
||||
## Documentation
|
||||
|
||||
### Control Folder README
|
||||
Each complex control folder includes README.md:
|
||||
|
||||
```markdown
|
||||
# NavigationView
|
||||
|
||||
Complex navigation container with 6 partial class files:
|
||||
|
||||
- **Base.cs** - Core control logic, template application
|
||||
- **Properties.cs** - 27 dependency properties
|
||||
- **Events.cs** - 7 routed events
|
||||
- **Navigation.cs** - Page navigation, journal, back/forward
|
||||
- **TemplateParts.cs** - Template part bindings
|
||||
- **AttachedProperties.cs** - HeaderContent attached property
|
||||
|
||||
Related types:
|
||||
- NavigationViewItem - Selectable item container
|
||||
- NavigationViewItemHeader - Non-selectable header
|
||||
- INavigationView - Public control interface
|
||||
```
|
||||
|
||||
## References
|
||||
- [Feature Folders in ASP.NET](https://docs.microsoft.com/archive/msdn-magazine/2016/september/asp-net-core-feature-slices-for-asp-net-core-mvc) (similar pattern)
|
||||
- [Vertical Slice Architecture](https://jimmybogard.com/vertical-slice-architecture/)
|
||||
Reference in New Issue
Block a user