# WinUI 3 Implementation Patterns
Detailed implementation patterns for WinUI 3. See `doc/WinUI/` for quick-reference rules.
---
## DataTemplateSelector Pattern
DataTemplateSelector classes MUST be marked `partial` for C#/WinRT compatibility.
### Problem
Without the `partial` keyword, DataTemplateSelector classes crash in Release builds due to C#/WinRT interop requirements.
### Solution
```csharp
using Microsoft.UI.Xaml.Controls;
namespace YourAppName
{
public partial class CustomTemplateSelector : DataTemplateSelector
{
public DataTemplate TextTemplate { get; set; }
public DataTemplate SeparatorTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item)
{
if (item is MyDataItem dataItem)
{
if (dataItem.IsSeparator)
{
return SeparatorTemplate;
}
else
{
return TextTemplate;
}
}
return base.SelectTemplateCore(item);
}
// The other overload should pass through to the base method
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return SelectTemplateCore(item);
}
}
}
```
### XAML Usage
```xaml
```
### Key Points
- Always mark selector classes as `partial`
- Use `SelectTemplateCore(object item)` for most cases
- Pass-through for the container overload
- Define templates as resources before instantiating selector
---
## Centered ContentDialog Title Pattern
ContentDialog titles are left-aligned by default. Use TitleTemplate to center them.
### Problem
ContentDialog's title alignment is not exposed as a simple property and is deeply ingrained in internal structure.
### Solution
```xaml
```
### Define CenteredTitleTemplate
```xaml
```
### Key Points
- TitleTemplate allows custom UI for the title area
- Grid with `HorizontalAlignment="Stretch"` provides full width
- TextBlock with `HorizontalAlignment="Center"` centers the text
- Text binding inherits the original Title property value
---
## TextBox Context Menu Pattern
Use built-in `TextCommandBarFlyout` for TextBox context menus instead of custom MenuFlyout.
### Problem
Custom "Select All" menu items in TextBox context menus don't work reliably because:
- TextBox loses focus when context menu opens
- Selection disappears due to event timing issues
- Manual `SelectAll()` calls are brittle
### Solution
```xaml
```
### Alternative (Manual Implementation)
If you truly need custom menu items:
```csharp
private void SelectAllMenuItem_Click(object sender, RoutedEventArgs e)
{
myTextBox.SelectAll();
}
```
However, the built-in `TextCommandBarFlyout` is preferred because it includes:
- Cut
- Copy
- Paste
- Select All
- Undo/Redo (when supported)
### Key Points
- `TextCommandBarFlyout` provides native editing commands
- Avoid custom MenuFlyout for TextBox editing operations
- Focus handling is complex - use built-in solutions
- TextCommandBarFlyout automatically handles focus and selection state
---
## Drag and Drop Reordering Pattern
Enable drag-and-drop reordering in ListView using built-in properties.
### Problem
Need to allow users to reorder items in a ListView, especially when items contain TextBox controls.
### Solution
```xaml
```
### Key Properties
| Property | Purpose |
| ------------------------ | --------------------------------------------- |
| `CanReorderItems="True"` | Enables user to reorder items within the list |
| `AllowDrop="True"` | Allows items to be dropped onto the control |
| `CanDragItems="True"` | Allows items to be dragged from the control |
### Backend Integration
Bind to a dynamic collection like `ObservableCollection`:
```csharp
public ObservableCollection MyCollection { get; set; }
public MyViewModel() {
MyCollection = new ObservableCollection {
new MyItem { MyTextProperty = "Item 1" },
new MyItem { MyTextProperty = "Item 2" },
new MyItem { MyTextProperty = "Item 3" },
};
}
```
When the user reorders items via the UI, the bound collection automatically updates to reflect the new order.
### Advanced Control
For fine-grained control:
```csharp
// Drag starting - can cancel specific items
MyListView.DragItemsStarting += (s, e) => {
if (ShouldCancelDrag(e.Items)) {
e.Cancel = true;
}
};
// Drag completed - react to reordering
MyListView.DragItemsCompleted += (s, e) => {
// Items have been moved - collection is already updated
LogReorder(e.Items, e.NewPosition);
};
```
### Key Points
- All three properties must be set for full drag-drop support
- TextBox controls inside DataTemplates work seamlessly
- ObservableCollection automatically tracks reordering
- Use events for validation or logging after reordering
---
## Elevated Script Launch Pattern
Launch a PowerShell script with elevation (UAC) without a visible terminal window.
### Problem
`Process.Start` with `Verb = "runas"` and `-WindowStyle Hidden` in the arguments is a race condition: the shell creates and shows the window before PowerShell gets around to processing the flag. The window flashes briefly on every save.
### Solution
Use `ShellExecuteExW` directly via P/Invoke. It accepts a `uShow` field (`SW_HIDE = 0`) that is applied at the shell level — before any child process window is created.
```csharp
[StructLayout(LayoutKind.Sequential)]
private struct ShellExecuteInfo {
public int Size; // Must be set to Marshal.SizeOf()
public uint Flags; // SEE_MASK_NOSHOWUI suppresses error dialogs
public IntPtr Window;
public IntPtr Verb; // "runas" — triggers UAC elevation
public IntPtr File; // "powershell.exe"
public IntPtr Parameters;
public IntPtr Directory;
public int Show; // SW_HIDE = 0 — the key difference from Process.Start
// ... remaining fields ...
public IntPtr Process; // Populated on return — wait on this handle
}
```
String fields (`Verb`, `File`, `Parameters`) are `LPCWSTR` pointers. Marshal them manually with `Marshal.StringToHGlobalUni` / `FreeHGlobal` in a try/finally — `LibraryImport` cannot marshal strings inside structs.
After the call: `WaitForSingleObject(info.Process, INFINITE)` → `GetExitCodeProcess` → `CloseHandle`.
### Key Points
- UAC prompt still appears (that's Windows enforcing elevation — unavoidable and expected)
- The PowerShell *terminal window* does not appear
- Non-elevated path (user-only vars) uses `Process.Start` with `CreateNoWindow = true` — simpler, no P/Invoke needed
- `SEE_MASK_NOSHOWUI` suppresses the shell's own error dialog if `ShellExecuteExW` fails
---
## Non-Shared Context Menu Pattern
Define context menus in a Style to ensure each item gets a unique instance.
### Problem
Defining a single `MenuFlyout` as a shared resource (`x:Key` in a dictionary) and referencing it via `StaticResource` in a `ListView` item template can cause rare "empty menu" issues. This happens because WinUI may struggle to re-bind the `DataContext` to the shared menu instance during rapid recycling.
### Solution
Wrap the `MenuFlyout` in a `Setter.Value` within a `Style`.
```xaml
```
### Key Points
- Ensures each item has its own flyout instance
- Fixes `DataContext` resolution race conditions
- Eliminates "empty menu" failures in recycled lists
---
## Incremental List Reconciliation Pattern
Update `ObservableCollection` items incrementally instead of clearing the whole list.
### Problem
Calling `Clear()` and then adding items to a bound collection causes the entire UI pane to flicker and lose scroll position/focus. $O(N^2)$ reconciliation (using `IndexOf` or `Contains` in a loop) becomes slow as lists grow.
### Solution
Use a $O(N)$ reconciliation approach with `HashSet` for lookups, and implement a "fast path" for bulk updates (like initial load or search).
```csharp
public void UpdateList(List targetList, T? changedItem = null) {
// Fast Path: Bulk update
if (changedItem == null) {
if (!Collection.SequenceEqual(targetList)) {
Collection.Clear();
foreach (var item in targetList) Collection.Add(item);
}
return;
}
// Incremental Path: Targeted update
var targetSet = new HashSet(targetList);
// 1. Remove missing
// 2. Move/Insert to match order
// 3. Force refresh of changedItem via indexer assignment: Collection[i] = item;
}
```
### Key Points
- Prevents UI flicker and scroll jumps
- $O(N)$ performance for large lists
- `Collection[i] = item` forces WinUI to re-evaluate the DataTemplate (useful for type toggles)
---
## Background System Notification Pattern
Always run blocking system-wide broadcasts on a background thread.
### Problem
Broadcasting a system-wide environment change (`WM_SETTINGCHANGE` via `SendMessageTimeout`) is synchronous and can take several seconds if other open applications are "hung" or slow to respond. This blocks the UI thread during every Save.
### Solution
Await a background task for the notification.
```csharp
public async Task SaveAsync(IEnumerable items) {
await Task.Run(() => PerformSave(items));
// System notification on background thread
await Task.Run(() => NotifySystemOfChanges());
}
```
### Key Points
- Prevents the application from "hanging" after a successful save
- Keeps the UI responsive while other apps process the change
---
## CommandBar Height Stabilization Pattern
Enforce explicit heights and collapsed labels to prevent layout shifts.
### Problem
WinUI `CommandBar` has complex internal logic for sizing based on label visibility and overflow. This can cause the bar (and the whole UI) to jump by a few pixels when search boxes are toggled or window sizes change.
### Solution
Use a consistent Style that locks the height and label behavior.
```xaml
```
### Key Points
- Prevents jarring layout shifts
- Provides a stable anchor for absolute-positioned elements (like Mica backdrops)
---
## ObservableProperty Initialization Pattern
Initialize complex `[ObservableProperty]` types in the constructor rather than inline.
### Problem
Initializing complex types (like `ObservableCollection`) or properties with `[NotifyPropertyChangedFor]` dependencies inline can cause `NullReferenceException` or unexpected behavior in WinUI 3. This happens because the source-generated property change handlers fire during the object's initialization phase, potentially accessing dependent objects that haven't been instantiated yet.
### Solution
Initialize simple types inline if desired, but always move complex types and dependent properties to the constructor.
```csharp
[ObservableProperty]
public partial string Name { get; set; } = string.Empty; // Simple types are safe inline
[ObservableProperty]
public partial ObservableCollection- Items { get; set; }
public MyViewModel() {
Items = []; // Complex types MUST be in constructor
}
```
### Key Points
- Prevents race conditions during WinUI 3 / MVVM Toolkit source generation.
- Simple types (`bool`, `int`, `string`) are safe for inline initialization.
- Collections and objects with `[NotifyPropertyChangedFor]` MUST use constructor initialization.
---
## XAML Binding Rules
### x:Bind vs {Binding}
**Use `x:Bind` in Window/Page XAML** (e.g. MainWindow.xaml):
```xaml
```
**Use `{Binding}` in ResourceDictionary/DataTemplate** (e.g. VariableTemplates.xaml):
```xaml
```
Never use `x:Bind` in ResourceDictionary files — it won't work reliably without a code-behind compilation context.
### Binding Modes
| Syntax | Default Mode | Use for |
|--------|-------------|---------|
| `{x:Bind}` | OneTime | Static values, computed properties |
| `{x:Bind ..., Mode=OneWay}` | OneWay | Observable properties (read-only UI) |
| `{x:Bind ..., Mode=TwoWay}` | TwoWay | Editable controls (TextBox, CheckBox) |
| `{Binding}` | OneWay | ResourceDictionary templates |
### Converter Declaration Order
Converters MUST be declared BEFORE MergedDictionaries in App.xaml:
```xaml
```
### ResourceDictionary Code-Behind
ResourceDictionaries using DataTemplates must have a code-behind class:
```csharp
namespace WinEnvEdit.Resources;
public partial class VariableTemplates : ResourceDictionary {
public VariableTemplates() {
InitializeComponent();
}
}
```
---
## Unpackaged Framework-Dependent Deployment Pattern
Configure WinUI 3 apps for minimal-size MSI deployment with external runtime dependencies.
### Goal
Create an **unpackaged, framework-dependent MSI** that:
- Is small (~10 MB) and doesn't bundle runtimes
- Relies on user-installed .NET 10 Desktop Runtime and Windows App SDK 1.8
- Benefits from system-level runtime patching and updates
- Distributes via WinGet with minimal dependencies
### Problem
WinUI 3 unpackaged apps have conflicting deployment requirements:
1. **Trimming doesn't work** - WinUI dependencies (WebView2, Windows SDK, WinRT) aren't trim-compatible, causing build errors
2. **Publish doesn't copy .pri files** - `dotnet publish` fails to include Package Resource Index files needed for WinUI initialization
3. **Self-contained is huge** - Bundling runtimes creates ~50-100 MB installers without trimming benefits
4. **Documentation is sparse** - Microsoft docs focus on packaged (MSIX) or self-contained deployments, not framework-dependent unpackaged
### Solution
Use **MSI (not MSIX)** for unsigned distribution via WinGet, with these **exact project properties**:
```xml
false
false
false
None
false
true
false
true
```
**Publish command:**
```powershell
dotnet publish -c Release -p:Platform=x64
# Note: NO -r flag (see "Why No Runtime Identifier?" below)
```
### Property Explanations
| Property | Value | Reason |
|----------|-------|--------|
| `SelfContained` | `false` | Framework-dependent - requires .NET 10 installed |
| `PublishTrimmed` | `false` | Trimming fails with WinUI dependencies (see below) |
| `WindowsPackageType` | `None` | Unpackaged app (not MSIX) |
| `WindowsAppSDKSelfContained` | `false` | Framework-dependent - requires Windows App SDK 1.8 installed |
| `WindowsAppSDKBootstrapInitialize` | `true` | Auto-initialize Bootstrap for framework-dependent apps |
| `WindowsAppSDKDeploymentManagerInitialize` | `false` | DeploymentManager requires package identity (we're unpackaged) |
| **`EnableMsixTooling`** | **`true`** | **CRITICAL:** Makes `dotnet publish` copy .pri files to output |
### Why EnableMsixTooling=true?
**Without this property**, `dotnet publish` fails to copy the Package Resource Index (.pri) file to the publish directory, causing WinUI initialization failures:
```
Exception code: 0xc000027b
Faulting module: Microsoft.UI.Xaml.dll
```
The .pri file contains embedded compiled XAML (.xbf) and resource metadata. WinUI can't initialize without it.
**With EnableMsixTooling=true**, the publish process correctly copies `WinEnvEdit.pri` even for unpackaged apps.
**Reference:** [WindowsAppSDK #3451 - Missing .pri file in publish](https://github.com/microsoft/WindowsAppSDK/issues/3451)
### Why No Runtime Identifier (-r flag)?
**Do NOT use** `dotnet publish -r win-x64` even for framework-dependent apps. The `-r` flag creates runtime-specific output that causes Windows App SDK bootstrap initialization failures.
**What happens with `-r win-x64`:**
- Creates `bin/.../win-x64/publish/` output (nested folders)
- Includes runtime-specific native binaries even with `SelfContained=false`
- **Breaks** Windows App SDK bootstrap with "package identity" errors:
```
Exception code: 0xc000027b (DLL_INIT_FAILED)
Faulting module: Microsoft.UI.Xaml.dll
```
**What happens without `-r`:**
- Creates `bin/.../publish/` output (clean structure)
- Pure managed assemblies (platform-agnostic)
- **Works** - Bootstrap initializes correctly
**Why the `-r` flag breaks framework-dependent apps:**
- The runtime identifier creates a hybrid deployment mode
- Windows App SDK bootstrap expects either fully self-contained OR fully framework-dependent
- The hybrid mode confuses the bootstrap initialization sequence
**When to use `-r`:**
- Self-contained deployments (`SelfContained=true`)
- Cross-platform apps targeting Linux/macOS
- Not for Windows-only framework-dependent WinUI apps
**For WinUI framework-dependent:** Use `dotnet publish` **without** `-r` for clean, working output.
### Why MSI Instead of MSIX?
**MSIX (packaged apps)** offer benefits like sandboxing, auto-updates, and cleaner installs, but have critical drawbacks for open-source distribution:
| Format | Signing Required? | Distribution | End-User Experience |
|--------|------------------|--------------|---------------------|
| **MSI (unpackaged)** | No (optional) | WinGet, direct download | ⚠️ SmartScreen warning (unsigned), but installs normally |
| **MSIX (packaged)** | **Yes** (for production) | Microsoft Store, WinGet | ✅ No warnings if signed, ❌ Requires Developer Mode if unsigned |
**Why MSI for this project:**
1. **No signing cost** - Code signing certificates cost $100-400/year
- EV certificates (~$300-400/year) give instant SmartScreen reputation
- OV certificates (~$100-200/year) require weeks/months to build reputation
2. **WinGet mitigates SmartScreen** - Installing via `winget install` provides inherent trust, reducing warnings
3. **Unsigned MSIX is impractical** - Requires end-users to enable Developer Mode (sideloading), which is a non-starter for general distribution
4. **MSI works everywhere** - Traditional installer, no special modes required
**Could we use MSIX?** Yes, if we had a code signing certificate. But for **unsigned open-source distribution**, MSI + WinGet is the practical choice.
**Reference:** [Windows packaging overview](https://learn.microsoft.com/en-us/windows/apps/package-and-deploy/)
### Why No Trimming?
Trimming (`PublishTrimmed=true`) **requires** `SelfContained=true` and fails with WinUI 3 apps due to incompatible dependencies:
**Trim errors:**
```
error IL2104: Assembly 'Microsoft.Web.WebView2.Core' produced trim warnings
error IL2104: Assembly 'Microsoft.Windows.SDK.NET' produced trim warnings
error IL2104: Assembly 'WinRT.Runtime' produced trim warnings
error NETSDK1144: Optimizing assemblies for size failed
```
**Why it fails:**
- WinUI dependencies use reflection and dynamic code generation
- WebView2, Windows SDK projections, and WinRT runtime aren't marked as trim-safe
- Even with `TrimMode=partial`, critical assemblies produce errors
**Alternatives considered:**
- Self-contained without trimming: ~50-100 MB MSI (too large)
- Self-contained with trimming: Build fails completely (not viable)
- Framework-dependent with trimming: Error "requires self-contained app" (not allowed)
**Conclusion:** Accept the ~10 MB framework-dependent output as the minimal viable size for WinUI 3 unpackaged apps.
**References:**
- [WindowsAppSDK #2478 - IL trimming support](https://github.com/microsoft/WindowsAppSDK/issues/2478)
- [WindowsAppSDK #5969 - Remove ML libraries from build](https://github.com/microsoft/WindowsAppSDK/issues/5969)
- [.NET Trimming documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained)
### Deployment Dependencies
**WinGet manifest dependencies:**
```yaml
Dependencies:
PackageDependencies:
- PackageIdentifier: Microsoft.DotNet.DesktopRuntime.10
- PackageIdentifier: Microsoft.WindowsAppRuntime.1.8
```
Users must install these runtimes (typically via WinGet) before running the app. The installer is small (~10 MB) and benefits from system-level runtime patching.
### Key Points
- **10 MB is excellent** for a WinUI 3 app with framework dependencies
- `dotnet publish` (without `-r` flag) creates clean, platform-agnostic output
- EnableMsixTooling=true is **required** even for unpackaged apps
- Trimming is **not viable** for WinUI 3 as of Windows App SDK 1.8
- Framework-dependent is the **recommended approach** for distribution via package managers
---
## Additional Resources
- [Microsoft Learn: Data template selection](https://learn.microsoft.com/en-us/windows/apps/develop/ui/controls/data-template-selector)
- [Windows App SDK: ContentDialog API](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.contentdialog?view=windows-app-sdk-1-8)
- [microsoft-ui-xaml GitHub Issues](https://github.com/microsoft/microsoft-ui-xaml/issues) - Check for known WinUI 3 bugs and workarounds
- [WindowsAppSDK #3451: Missing .pri file in publish](https://github.com/microsoft/WindowsAppSDK/issues/3451)
- [WindowsAppSDK #2478: IL trimming support](https://github.com/microsoft/WindowsAppSDK/issues/2478)
- [Deploy unpackaged apps guide](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/deploy-unpackaged-apps)