# 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)