# WinUI 3 migration plan **Started:** 2026-05-12 (overnight) **Status:** in flight — scaffold + redesigned MainWindow + theme system landed, runtime activation blocked, view-model wiring not yet started. The full plan for replatforming TeamsISO from WPF / .NET 8 to WinUI 3 / Windows App SDK 1.6 LTS. The redesigned UI per the approved shape brief (PRODUCT.md, DESIGN.md, the 2026-05-12 chat transcript) lands as the new TeamsISO.App.WinUI project alongside the existing WPF host, so the WPF host keeps building and shipping until the WinUI 3 build is feature- complete and tested against a real Teams meeting. ## Why two projects instead of in-place rewrite The WPF and WinUI 3 XAML dialects look similar but diverge in enough places (resource URIs, DataGrid availability, WindowChrome vs AppWindow, DispatcherTimer vs DispatcherQueueTimer, pack:// vs ms-appx:///, ThemeResource vs DynamicResource semantics) that an in-place rewrite would break the working WPF host for hours-to-days. Coexisting both projects means: 1. `dotnet build TeamsISO.Windows.slnf` keeps producing a working WPF .exe throughout the migration. 2. Each WinUI 3 view can be migrated and verified independently. 3. The engine layer (TeamsISO.Engine, TeamsISO.Engine.NdiInterop) and the view-models (TeamsISO.App/ViewModels/) are **shared** via ProjectReference. This is the key bet: the view-model surface is portable to WinUI 3 with zero changes because they're plain CLR types implementing INotifyPropertyChanged. 4. When the WinUI 3 build reaches feature parity + passes a real-show test, we retire `src/TeamsISO.App` and the WinUI 3 project becomes the only shipping host. ## Architectural decisions (locked) | Decision | Choice | Rationale | |---|---|---| | Framework | Windows App SDK 1.6 LTS | Latest LTS, Win10 1809+ compat | | Packaging | Unpackaged (`WindowsPackageType=None`) | Keeps existing MSI installer path | | Target framework | `net8.0-windows10.0.19041.0` | WindowsAppSDK 1.6 minimum | | Platform floor | Win10 17763 (1809) | Working broadcast hardware | | RuntimeIdentifier | `win-x64` (pinned) | Flattens native DLLs to output dir | | Theme strategy | `ThemeDictionary` (Default = Dark, Light) | Built-in {ThemeResource} swap | | DataGrid | `CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2` | Only maintained free option | | View-model | Reuse from TeamsISO.App via ProjectReference | Zero porting cost | | Window chrome | `AppWindow.TitleBar.ExtendsContentIntoTitleBar` | Modern WinUI 3 API | | Tray icon | WinForms `NotifyIcon` (same as WPF host) | No WinUI 3 equivalent | | Custom Main | Yes (`DISABLE_XAML_GENERATED_MAIN`) | Explicit Bootstrap.TryInitialize | ## Phases ### Phase 1 — Scaffold (done) - [x] `src/TeamsISO.App.WinUI/` project created with WindowsAppSDK 1.6 - [x] `Themes/Tokens.xaml` with Dark + Light ThemeDictionaries - [x] `Themes/Controls.xaml` with Button hierarchy + typographic ramp - [x] `App.xaml` + `App.xaml.cs` minimal startup - [x] `Program.cs` custom Main with Bootstrap.TryInitialize - [x] Assets copied (Inter.ttf, JetBrainsMono.ttf, dragon-mark.png, icon) - [x] Solution updated (.sln + .slnf paths backslash-normalized) - [x] `dotnet build TeamsISO.Windows.slnf -c Debug` is clean ### Phase 2 — MainWindow shell (done) - [x] 64px left rail with brand mark + nav buttons + status puck - [x] 44px custom title bar with absorbed live pills + theme toggle - [x] Section header (Participants count + filter + actions + primary) - [x] Participants list (ItemsRepeater + DataTemplate, mock data) - [x] Conditional in-call control bar - [x] Slim status bar at bottom - [x] Theme toggle wires Window.Content.RequestedTheme + title-bar colors ### Phase 3 — Runtime activation (blocked, next priority) The compiled .exe shows "TeamsISO.exe - This application could not be started" before Main() runs. COREHOST_TRACE confirms .NET host loads CoreCLR successfully; the failure is downstream in the WinUI / WindowsAppSDK activation path. Suspected causes (in priority order): 1. **Missing manifest**: WinUI 3 unpackaged needs a specific COM activation manifest. Our custom `app.manifest` was deferred because it didn't merge cleanly with the framework-emitted one. Reintroduce with proper `uap:VisualElements`. 2. **Microsoft.WindowsDesktop.App framework reference**: runtimeconfig.json includes `Microsoft.WindowsDesktop.App 8.0.0`, which WinUI 3 doesn't want. The .NET SDK adds it implicitly from the `-windows` target framework moniker. Try `true` + remove from frameworks list. 3. **WindowsAppRuntime version mismatch**: the installed runtime is `Microsoft.WindowsAppRuntime.1.6 (6000.519.329.0)`. Bootstrap.TryInitialize should accept any 1.6.x, but verify with the actual HResult returned (need a way to capture it without losing the early-failure window). 4. **Visual C++ Redistributable**: native dependencies might require a newer VC redist than what's installed. Check WindowsAppSDK 1.6's redist requirements. **Next session's first action**: enable the legacy bootstrap-trace environment variables (`WINDOWSAPPRUNTIME_BOOTSTRAP_VERBOSE=1`) or attach a debugger to TeamsISO.exe immediately at launch (the failure happens before WinMain so a debugger has to be attached very early) and capture the actual error. ### Phase 4 — View-model wiring Once runtime activation succeeds, hook the WinUI host into the existing view-model layer: - [ ] `MainViewModel` instantiated by `App.OnLaunched` (mirror WPF App.xaml.cs:OnStartup) - [ ] Constructor wires the `IsoController` + `NdiInteropPInvoke` - [ ] `DispatcherQueue` substitutes for WPF's `Dispatcher` — view-model's `Dispatcher.InvokeAsync` calls need adapting to `DispatcherQueue.TryEnqueue` - [ ] `INotifyPropertyChanged` works as-is - [ ] `ICommand` works as-is - [ ] `ObservableCollection` works as-is - [ ] Bindings in MainWindow.xaml updated from {Binding ...} to {x:Bind ...} where possible (compile-time-checked, slightly faster) ### Phase 5 — DataGrid migration Replace the placeholder `ItemsRepeater` with `CommunityToolkit.WinUI.UI.Controls.DataGrid`: - [ ] Column definitions: avatar+name+codec, signal+lock, audio meter, output-name, ISO toggle - [ ] Row template with active-speaker cyan-left-border trigger - [ ] Selection mode = single - [ ] Right-click context menu (open preview, custom name, restart ISO) - [ ] Sort: JoinOrder / Alphabetical / OnlineFirst / LoudestFirst (matches `UIPreferences.SortMode`) ### Phase 6 — Secondary windows - [ ] Settings drawer (`SettingsDrawer.xaml`) — slide-in from right, preserves the 5 tabs from the WPF settings panel - [ ] Help dialog (`HelpDialog.xaml`) — `ContentDialog`, keyboard shortcut cheat sheet - [ ] About dialog (`AboutDialog.xaml`) — version, logs path, update check - [ ] Onboarding (`OnboardingWindow.xaml`) — first-launch only, three panes - [ ] Notes viewer (`NotesViewer.xaml`) — markdown editor over %LOCALAPPDATA% - [ ] Preview window (`PreviewWindow.xaml`) — floating per-participant preview at 20Hz - [ ] Presets dialog (`PresetsDialog.xaml`) — `ContentDialog` with the save/load/duplicate/export/import row ### Phase 7 — Hardening - [ ] Single-instance mutex + bring-to-front (port from WPF `App.xaml.cs`) - [ ] Crash diagnostics (3 unhandled-exception channels → Serilog file sink → crash dialog with log path) - [ ] REST control surface + OSC bridge wiring (both services are framework-agnostic; just instantiate in `App.OnLaunched`) - [ ] Tray icon (port `TrayIconHost.cs` — WinForms.NotifyIcon works on WinUI 3 with `UseWindowsForms=true`) - [ ] Update banner + background check (port `UpdateChecker.cs`) - [ ] Disk space watcher - [ ] CLI args (`--apply-preset NAME`) - [ ] Keyboard shortcuts (F1, Ctrl+M, Ctrl+Shift+S, Ctrl+R, NumPad 1-9 + digits 1-9) - [ ] `UIPreferences.Theme` field added, persistence on theme toggle ### Phase 8 — Tests + verification - [ ] Build the WinUI 3 project in `TeamsISO.App.Tests` (currently targets `net8.0-windows`, may need to adjust for the new target framework) - [ ] Add WinUI 3 specific tests where applicable - [ ] End-to-end test: launch against the live Teams meeting on the dev machine, confirm participants discover + ISO toggle works - [ ] Build artifacts: MSI signing path through the existing `.forgejo/workflows/release.yml` ### Phase 9 — Retire WPF host - [ ] `dotnet sln remove src/TeamsISO.App/TeamsISO.App.csproj` - [ ] Delete `src/TeamsISO.App/` directory - [ ] Update README.md and CHANGELOG.md - [ ] Tag v1.0.0 (the original v1.0 cut moves to v0.9; v1.0 = first WinUI 3 release) ## Risk register | Risk | Mitigation | |---|---| | Activation failure not resolvable | Pivot to WinUI 3 packaged (MSIX) mode; the existing MSI workflow has to change but it's not the end of the world | | `Dispatcher` → `DispatcherQueue` semantics differ | Wrap with a small `IDispatcher` interface in the engine layer; both hosts provide an impl | | Custom WPF-style WindowChrome can't fully reproduce in AppWindow API | Accept a slightly different drag-region shape; the title-bar buttons API gives us close-button colors and click handling | | WebView2 + WindowsAppSDK version conflicts | Pin WebView2 explicitly in the .csproj | | CommunityToolkit DataGrid 7.x maintenance ending | Plan a fallback to `WinUI.TableView` 1.4.x as a contingency | | Performance regression on the participants table (thumbnails at 20Hz × N rows) | Profile early; if needed, use `Win2D` for the audio meter and signal indicator | ## What I'm NOT doing - Replacing the engine layer - Touching the NDI native interop - Changing the control surface protocol (REST/WebSocket/OSC) - Migrating tests right now (Phase 8) - Adding new product features (anything not in the redesign brief stays for a follow-on release)