# Threading and Window Management Migration Based on patterns from the ImageResizer migration. ## Dispatcher → DispatcherQueue ### API Mapping | WPF | WinUI 3 | |-----|---------| | `Dispatcher.Invoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` | | `Dispatcher.BeginInvoke(Action)` | `DispatcherQueue.TryEnqueue(Action)` | | `Dispatcher.Invoke(DispatcherPriority, Action)` | `DispatcherQueue.TryEnqueue(DispatcherQueuePriority, Action)` | | `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | | `Dispatcher.VerifyAccess()` | Check `DispatcherQueue.HasThreadAccess` (no exception-throwing method) | ### Priority Mapping WinUI 3 has only 3 levels: `High`, `Normal`, `Low`. | WPF `DispatcherPriority` | WinUI 3 `DispatcherQueuePriority` | |-------------------------|----------------------------------| | `Send` | `High` | | `Normal` / `Input` / `Loaded` / `Render` / `DataBind` | `Normal` | | `Background` / `ContextIdle` / `ApplicationIdle` / `SystemIdle` | `Low` | ### Pattern: Global DispatcherQueue Access (from ImageResizer) WPF provided `Application.Current.Dispatcher` globally. WinUI 3 requires explicit storage: ```csharp // Store DispatcherQueue at app startup private static DispatcherQueue _uiDispatcherQueue; public static void InitializeDispatcher() { _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread(); } ``` Usage with thread-check pattern (from `Settings.Reload()`): ```csharp var currentDispatcher = DispatcherQueue.GetForCurrentThread(); if (currentDispatcher != null) { // Already on UI thread ReloadCore(jsonSettings); } else if (_uiDispatcherQueue != null) { // Dispatch to UI thread _uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings)); } else { // Fallback (e.g., CLI mode, no UI) ReloadCore(jsonSettings); } ``` ### Pattern: DispatcherQueue in ViewModels (from ProgressViewModel) ```csharp public class ProgressViewModel { private readonly DispatcherQueue _dispatcherQueue; public ProgressViewModel() { _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); } private void OnProgressChanged(double progress) { _dispatcherQueue.TryEnqueue(() => { Progress = progress; // other UI updates... }); } } ``` ### Pattern: Async Dispatch (await) ```csharp // WPF await this.Dispatcher.InvokeAsync(() => { /* UI work */ }); // WinUI 3 (using TaskCompletionSource) var tcs = new TaskCompletionSource(); this.DispatcherQueue.TryEnqueue(() => { try { /* UI work */ tcs.SetResult(); } catch (Exception ex) { tcs.SetException(ex); } }); await tcs.Task; ``` ### C++/WinRT Threading | Old API | New API | |---------|---------| | `winrt::resume_foreground(CoreDispatcher)` | `wil::resume_foreground(DispatcherQueue)` | | `CoreDispatcher.RunAsync()` | `DispatcherQueue.TryEnqueue()` | Add `Microsoft.Windows.ImplementationLibrary` NuGet for `wil::resume_foreground`. --- ## Window Management ### WPF Window vs WinUI 3 Window | Feature | WPF `Window` | WinUI 3 `Window` | |---------|-------------|------------------| | Base class | `ContentControl` → `DependencyObject` | **NOT** a control, NOT a `DependencyObject` | | `Resources` property | Yes | No — use root container's `Resources` | | `DataContext` property | Yes | No — use root `Page`/`UserControl` | | `VisualStateManager` | Yes | No — use inside child controls | | `Load`/`Unload` events | Yes | No | | `SizeToContent` | Yes (`Height`/`Width`/`WidthAndHeight`) | No — must implement manually | | `WindowState` (min/max/normal) | Yes | No — use `AppWindow.Presenter` | | `WindowStyle` | Yes | No — use `AppWindow` title bar APIs | | `ResizeMode` | Yes | No — use `AppWindow.Presenter` | | `WindowStartupLocation` | Yes | No — calculate manually | | `Icon` | `Window.Icon` | `AppWindow.SetIcon()` | | `Title` | `Window.Title` | `AppWindow.Title` (or `Window.Title`) | | Size (Width/Height) | Yes | No — use `AppWindow.Resize()` | | Position (Left/Top) | Yes | No — use `AppWindow.Move()` | | `IsDefault`/`IsCancel` on buttons | Yes | No — handle Enter/Escape in code-behind | ### Getting AppWindow from Window ```csharp using Microsoft.UI; using Microsoft.UI.Windowing; using WinRT.Interop; IntPtr hwnd = WindowNative.GetWindowHandle(window); WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); AppWindow appWindow = AppWindow.GetFromWindowId(windowId); ``` ### Pattern: SizeToContent Replacement (from ImageResizer) WinUI 3 has no `SizeToContent`. ImageResizer implemented a manual equivalent: ```csharp private void SizeToContent() { if (Content is not FrameworkElement content) return; // Measure desired content size content.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); var desiredHeight = content.DesiredSize.Height + WindowChromeHeight + Padding; // Account for DPI scaling var scaleFactor = Content.XamlRoot.RasterizationScale; var pixelHeight = (int)(desiredHeight * scaleFactor); var pixelWidth = (int)(WindowWidth * scaleFactor); // Resize via AppWindow var hwnd = WindowNative.GetWindowHandle(this); var windowId = Win32Interop.GetWindowIdFromWindow(hwnd); var appWindow = AppWindow.GetFromWindowId(windowId); appWindow.Resize(new Windows.Graphics.SizeInt32(pixelWidth, pixelHeight)); } ``` **Key details:** - `WindowChromeHeight` ≈ 32px for the title bar - Must multiply by `RasterizationScale` for DPI-aware sizing - Call `SizeToContent()` after page navigation or content changes - Unsubscribe previous event handlers before subscribing new ones to avoid memory leaks ### Window Positioning (Center Screen) ```csharp var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest); var centerX = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2; var centerY = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2; appWindow.Move(new Windows.Graphics.PointInt32(centerX, centerY)); ``` ### Window State (Minimize/Maximize) ```csharp (appWindow.Presenter as OverlappedPresenter)?.Maximize(); (appWindow.Presenter as OverlappedPresenter)?.Minimize(); (appWindow.Presenter as OverlappedPresenter)?.Restore(); ``` ### Title Bar Customization ```csharp // Extend content into title bar this.ExtendsContentIntoTitleBar = true; this.SetTitleBar(AppTitleBar); // AppTitleBar is a XAML element // Or via AppWindow API if (AppWindowTitleBar.IsCustomizationSupported()) { var titleBar = appWindow.TitleBar; titleBar.ExtendsContentIntoTitleBar = true; titleBar.ButtonBackgroundColor = Colors.Transparent; } ``` ### Tracking the Main Window ```csharp public partial class App : Application { public static Window MainWindow { get; private set; } protected override void OnLaunched(LaunchActivatedEventArgs args) { MainWindow = new MainWindow(); MainWindow.Activate(); } } ``` ### ContentDialog Requires XamlRoot ```csharp var dialog = new ContentDialog { Title = "Confirm", Content = "Are you sure?", PrimaryButtonText = "Yes", CloseButtonText = "No", XamlRoot = this.Content.XamlRoot // REQUIRED }; var result = await dialog.ShowAsync(); ``` ### File Pickers Require HWND ```csharp var picker = new FileOpenPicker(); picker.FileTypeFilter.Add(".jpg"); // REQUIRED for desktop apps var hwnd = WindowNative.GetWindowHandle(App.MainWindow); WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd); var file = await picker.PickSingleFileAsync(); ``` ### Window Close Handling ```csharp // WPF protected override void OnClosing(CancelEventArgs e) { e.Cancel = true; this.Hide(); } // WinUI 3 this.AppWindow.Closing += (s, e) => { e.Cancel = true; this.AppWindow.Hide(); }; ``` --- ## Custom Entry Point (DISABLE_XAML_GENERATED_MAIN) ImageResizer uses a custom `Program.cs` entry point instead of the WinUI 3 auto-generated `Main`. This is needed for: - CLI mode (process files without showing UI) - Custom initialization before the WinUI 3 App starts - Single-instance enforcement ### Setup In `.csproj`: ```xml DISABLE_XAML_GENERATED_MAIN,TRACE ``` Create `Program.cs`: ```csharp public static class Program { [STAThread] public static int Main(string[] args) { if (args.Length > 0) { // CLI mode — no UI return RunCli(args); } // GUI mode WinRT.ComWrappersSupport.InitializeComWrappers(); Application.Start((p) => { var context = new DispatcherQueueSynchronizationContext( DispatcherQueue.GetForCurrentThread()); SynchronizationContext.SetSynchronizationContext(context); _ = new App(); }); return 0; } } ``` ### WPF App Constructor Removal WPF modules often created `new App()` to initialize the WPF `Application` and get `Application.Current.Dispatcher`. This is no longer needed — the WinUI 3 `Application.Start()` handles this. ```csharp // DELETE (WPF pattern): _imageResizerApp = new App(); // REPLACE with: Store DispatcherQueue explicitly (see Global DispatcherQueue Access above) ```