更新客户端渲染,更新了壳

This commit is contained in:
QWQLwToo
2026-07-06 23:05:40 +08:00
parent e7dd87bf7e
commit 31d778710b
1311 changed files with 172662 additions and 1582 deletions
@@ -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/)