mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-08-29 10:09:43 +02:00
docs(skills): recommend WindowEx for WinUI 3 migrations (#49303)
## Summary of the Pull Request Updates the WPF-to-WinUI 3 migration skill to make the established PowerToys windowing pattern explicit: - Default WinUI 3 top-level windows to `WinUIEx.WindowEx` or an existing PowerToys base derived from it, such as `TransparentWindow` for transient overlays. - Keep supported size, presenter, title bar, topmost, backdrop, and persistence behavior declarative in XAML instead of manual `AppWindow` / `OverlappedPresenter` code-behind. - Document the centrally managed `<PackageReference Include="WinUIEx" />`, WPF-to-`WindowEx` property mappings, and existing repository examples. - Add a value-converter decision guide that prefers `VisualStateManager`, direct `x:Bind` conversion, WinUI theme resources, and `CommunityToolkit.WinUI.Converters` over mechanically porting WPF converters. This follows the migration-skill feedback from @niels9001 in [PR #49174](https://github.com/microsoft/PowerToys/pull/49174#discussion_r3535181101). ## PR Checklist - [x] **Communication:** This change follows review feedback from a core contributor in PR #49174. - [x] **Tests:** The updated guidance passed 5/5 fresh agent migration scenarios. - [x] **Dev docs:** Added/updated. ## Detailed Description of the Pull Request / Additional comments `SKILL.md` now states the default PowerToys pattern and limits raw `AppWindow` / presenter code to behavior that `WindowEx` does not expose. The package mapping reference records the exact centrally managed dependency. The windowing reference adds a complete XAML example, a WPF-to-`WindowEx` mapping table, regular-window examples, and the `TransparentWindow` overlay exception. The XAML migration reference now also documents converter selection and reuse: control state belongs in `VisualState`s, count visibility can share one Toolkit `DoubleToVisibilityConverter` with `ConverterParameter=True`, and corner-radius converter resources come from `XamlControlsResources`. This is an atomic documentation-only change; no product code or dependencies are modified. ## Validation Steps Performed - Verified the documented `WindowEx` APIs and `TransparentWindow` inheritance against the repository and compiled WinUIEx assembly. - Verified all cited repository paths, the Markdown anchor, and central package management entry. - Ran five fresh current-branch agent scenarios; all selected `<PackageReference Include="WinUIEx" />` without a version, `WindowEx`, XAML-declared properties, and `CenterOnScreen()` only where required. - Ran a focused converter migration scenario before and after the guidance update; the updated skill selected `VisualState`s, one reusable Toolkit numeric converter, and existing WinUI corner-radius resources without custom converters. - Ran `git diff --check` with no errors. - Completed an independent read-only review with no findings. - Product builds and unit tests were not run because this change only updates agent guidance. --------- Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: fe9b24eb-99f0-43e8-aaa4-839f4579f6f9 Copilot-Session: d5afc36b-3356-46af-b8cb-071b87a64532
This commit is contained in:
@@ -57,6 +57,7 @@ These rules capture human judgment and must be applied consistently across every
|
||||
- **Do NOT instantiate services directly** — Use DI and CommunityToolkit.Mvvm patterns.
|
||||
- **Do NOT create a `Window` subclass for every dialog or sub-page** — use `ContentDialog` for in-app dialogs and `Frame`/`Page` navigation for sub-views. Separate `Window` classes are reserved for distinct top-level surfaces (e.g., FancyZones editor, OOBE).
|
||||
- **Do NOT omit `WindowsPackageType=None` and `WindowsAppSDKSelfContained=true`** — Both are mandatory in the csproj for every WinUI 3 module in PowerToys. Without them the app crashes at startup with `COMException: ClassFactory cannot supply requested class` because the WinUI 3 runtime DLLs are not found.
|
||||
- **Do NOT default to a bare `Window`, and do NOT hand-write code-behind for windowing that XAML can express** — For a top-level window in a WinUI 3 module, use `WinUIEx.WindowEx` or an existing PowerToys base derived from it (for example, `TransparentWindow` for transient overlays). `WindowEx` restores WPF-like `Window` members as XAML properties (`MinWidth`/`MinHeight`, `Width`/`Height`, `IsResizable`, `IsMaximizable`/`IsMinimizable`, `IsTitleBarVisible`, `IsAlwaysOnTop`, `IsShownInSwitchers`, `WindowState`, `SystemBackdrop`) plus helpers (`CenterOnScreen()`, `PersistenceId`), so windowing is declared in XAML instead of manual `AppWindow`/`OverlappedPresenter` code-behind. This is the established PowerToys convention across ImageResizer, PowerDisplay, Peek, AdvancedPaste, MeasureTool, ShortcutGuide, Settings, Hosts, FileLocksmith, QuickAccess, and other WinUI 3 modules. Only drop to raw `AppWindow`/presenter code for behavior `WindowEx` does not expose. See [Threading and Window Management → WindowEx](./references/threading-and-windowing.md#prefer-windowex-over-bare-window).
|
||||
|
||||
**XAML prohibitions:**
|
||||
- **Do NOT use `{DynamicResource}`** — Replace with `{ThemeResource}` (theme-reactive) or `{StaticResource}`.
|
||||
@@ -66,6 +67,7 @@ These rules capture human judgment and must be applied consistently across every
|
||||
- **Do NOT use `clr-namespace:`** — Replace with `using:` in all xmlns declarations.
|
||||
- **Do NOT use `Style.Triggers` / `DataTrigger` / `EventTrigger`** — Replace with `VisualStateManager`.
|
||||
- **Do NOT use `MultiBinding`** — Replace with `x:Bind` function binding or computed ViewModel property.
|
||||
- **Do NOT mechanically port WPF `IValueConverter` classes** — Prefer control `VisualState`s, direct `{x:Bind}` Boolean-to-`Visibility` conversion, resources supplied by `XamlControlsResources`, or `CommunityToolkit.WinUI.Converters`. Reuse converter instances and invert them with `ConverterParameter=True` when supported; write a custom converter only for app-specific conversion logic. See [Value Converter Decision Guide](./references/xaml-migration.md#value-converter-decision-guide).
|
||||
- **Do NOT use `Visibility="Hidden"`** — WinUI only has `Visible` and `Collapsed`. Use `Opacity="0"` if layout must be preserved.
|
||||
- **Do NOT use `IsDefault` / `IsCancel`** — Use `AccentButtonStyle` for primary button; handle Enter/Escape in code-behind.
|
||||
- **Do NOT omit `BasedOn` when overriding default styles** — Without it, your style replaces the entire default. Always use `BasedOn="{StaticResource DefaultButtonStyle}"` etc.
|
||||
@@ -212,7 +214,7 @@ These WPF features have no WinUI counterpart and require redesign, not find-and-
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.WindowsAppSDK` | Required |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `WinUIEx` | **Recommended for top-level windows in WinUI 3 modules** — add `<PackageReference Include="WinUIEx" />` (the version is centrally managed), then use `WindowEx` or an existing PowerToys base derived from it. It exposes WPF-like window properties in XAML (size, min/max, resizable, title-bar visibility, backdrop, always-on-top) plus `CenterOnScreen()`/`PersistenceId`; prefer it over bare `Window` and manual `AppWindow` code-behind |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox` |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.HeaderedControls` | Optional — `HeaderedContentControl`, `HeaderedItemsControl`, `HeaderedTreeView` |
|
||||
|
||||
@@ -161,7 +161,7 @@ Other primitives in `Primitives`: `ConstrainedBox`, `SwitchPresenter`, `WrapLayo
|
||||
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
|
||||
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
|
||||
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
|
||||
| (none) | `WinUIEx` | Optional, window helpers |
|
||||
| (none) | `WinUIEx` | **Recommended for top-level windows in WinUI 3 modules** — add `<PackageReference Include="WinUIEx" />` without a version because PowerToys manages it centrally; use `WindowEx` or an existing PowerToys base derived from it, and prefer XAML properties over bare `Window` and manual `AppWindow` code-behind |
|
||||
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
|
||||
| (none) | `CommunityToolkit.WinUI.Controls.Primitives` | Optional — `WrapPanel`, `UniformGrid`, `DockPanel`, `ConstrainedBox`, `SwitchPresenter` |
|
||||
|
||||
@@ -482,6 +482,7 @@ If the module communicates with the runner or settings UI:
|
||||
- [ ] Replace `{DynamicResource}` → `{ThemeResource}`
|
||||
- [ ] Replace `DataType="{x:Type ...}"` → `x:DataType="..."`
|
||||
- [ ] Replace `<Style.Triggers>` → `VisualStateManager`
|
||||
- [ ] Audit WPF converters: prefer control `VisualState`s, direct `{x:Bind}` Boolean conversion, `XamlControlsResources`, or CommunityToolkit converters; reuse invertible converter instances
|
||||
- [ ] Add `<XamlControlsResources/>` to `App.xaml` merged dictionaries
|
||||
- [ ] Move `Window.Resources` to root container's `Resources`
|
||||
- [ ] Run XamlStyler: `.\.pipelines\applyXamlStyling.ps1 -Main`
|
||||
|
||||
@@ -110,6 +110,56 @@ Add `Microsoft.Windows.ImplementationLibrary` NuGet for `wil::resume_foreground`
|
||||
|
||||
## Window Management
|
||||
|
||||
### Prefer `WindowEx` over bare `Window`
|
||||
|
||||
> **For top-level windows in WinUI 3 modules, default to `WinUIEx.WindowEx` or an existing PowerToys base derived from it, not bare `Window`.** Use `Microsoft.PowerToys.Common.UI.Controls.Window.TransparentWindow` when its transient-overlay behavior applies. Most rows in the table below say "No — use `AppWindow`…", which pushes windowing logic into code-behind. `WindowEx` restores many of these as **XAML properties**, so you declare windowing in XAML instead of hand-writing `AppWindow`/`OverlappedPresenter` code. Only drop to the raw `AppWindow`/presenter calls shown later for behavior `WindowEx` does not expose.
|
||||
|
||||
PowerToys centrally manages the WinUIEx version. Reference the package without specifying one:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="WinUIEx" />
|
||||
```
|
||||
|
||||
Declare a regular top-level window as `WindowEx` and set properties inline:
|
||||
|
||||
```xml
|
||||
<winuiex:WindowEx
|
||||
x:Class="MyApp.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
MinWidth="480"
|
||||
MinHeight="320"
|
||||
IsMaximizable="False"
|
||||
IsResizable="False"
|
||||
IsTitleBarVisible="False">
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<Grid>
|
||||
<!-- content -->
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
```
|
||||
|
||||
These WPF `Window` members become XAML properties on `WindowEx` (no code-behind):
|
||||
|
||||
| WPF `Window` | `WindowEx` (XAML unless noted) |
|
||||
|--------------|-------------------------------|
|
||||
| `MinWidth` / `MinHeight` | `MinWidth` / `MinHeight` |
|
||||
| `Width` / `Height` | `Width` / `Height` |
|
||||
| `ResizeMode="NoResize"` | `IsResizable="False"` |
|
||||
| `WindowStyle` min/max buttons | `IsMaximizable` / `IsMinimizable` |
|
||||
| `WindowState` | `WindowState` (`Normal`/`Minimized`/`Maximized`) |
|
||||
| `Topmost` | `IsAlwaysOnTop` |
|
||||
| `ShowInTaskbar` | `IsShownInSwitchers` (Alt-Tab visibility) |
|
||||
| custom / hidden title bar | `IsTitleBarVisible` |
|
||||
| backdrop | `<Window.SystemBackdrop><MicaBackdrop/></Window.SystemBackdrop>` |
|
||||
| `WindowStartupLocation="CenterScreen"` | `CenterOnScreen()` (method) |
|
||||
| persist size/position across sessions | `PersistenceId` (string) |
|
||||
|
||||
Regular-window examples in the repo: `src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml`, `src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/MainWindow.xaml`, `src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml`, `src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml`, `src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/MainWindow.xaml`. For a derived-base overlay example, see `src/modules/poweraccent/PowerAccent.UI/PowerAccentXAML/MainWindow.xaml`.
|
||||
|
||||
### WPF Window vs WinUI 3 Window
|
||||
|
||||
| Feature | WPF `Window` | WinUI 3 `Window` |
|
||||
|
||||
@@ -232,6 +232,37 @@ Key packages (XAML namespace is `using:CommunityToolkit.WinUI.Controls` for the
|
||||
- **`CommunityToolkit.WinUI.Behaviors`** — XAML behaviors for animations and interactions
|
||||
- **`CommunityToolkit.WinUI.Extensions`** — Extension methods for WinUI types
|
||||
|
||||
### Value Converter Decision Guide
|
||||
|
||||
Do not port WPF converters one-for-one. Use the first matching WinUI 3 option:
|
||||
|
||||
| Conversion need | Preferred migration |
|
||||
|-----------------|---------------------|
|
||||
| Control state inside a `ControlTemplate` (`IsSelected`, pointer-over, pressed) | Use the control's `VisualState`s and setters. Keep the element's base value (for example, `Visibility="Collapsed"`) and set the selected states to `Visible`; do not bind the state through a converter. |
|
||||
| `bool` → `Visibility` with `{x:Bind}` property binding | Bind the Boolean directly. The XAML compiler maps `true` to `Visible` and `false` to `Collapsed`. This is not a public reusable converter class and does not replace classic `{Binding}` / `TemplatedParent` scenarios. |
|
||||
| `bool` → `Visibility` with classic `{Binding}` | Use `CommunityToolkit.WinUI.Converters.BoolToVisibilityConverter`. |
|
||||
| Number or collection count → `Visibility` | Use `CommunityToolkit.WinUI.Converters.DoubleToVisibilityConverter` with `GreaterThan` / `LessThan`; it accepts numeric values such as `ObservableCollection.Count`. |
|
||||
| Inverse of an existing Toolkit Boolean or numeric conversion | Reuse the same converter with `ConverterParameter=True`. The Toolkit `BoolToObjectConverter` and `DoubleToObjectConverter` families support inversion; do not create a second instance with swapped values. |
|
||||
| Framework `CornerRadius` → `Rectangle.RadiusX` / `RadiusY` | Keep the `TopLeftCornerRadiusDoubleValueConverter` and `BottomRightCornerRadiusDoubleValueConverter` static resources supplied by `XamlControlsResources`; do not reimplement them. |
|
||||
| App-specific conversion with no platform or Toolkit equivalent | Implement a WinUI `IValueConverter`; its final parameter is `string language`, not WPF's `CultureInfo culture`. |
|
||||
|
||||
Add the centrally managed package only when a Toolkit converter is needed:
|
||||
|
||||
```xml
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
```
|
||||
|
||||
Use the `using:CommunityToolkit.WinUI.Converters` XAML namespace. Toolkit visibility converters already default to `Visible` / `Collapsed`, so set `TrueValue` or `FalseValue` only when behavior differs. Reuse one converter for normal and inverse count visibility:
|
||||
|
||||
```xml
|
||||
<converters:DoubleToVisibilityConverter
|
||||
x:Key="CountToVisibilityConverter"
|
||||
GreaterThan="0" />
|
||||
|
||||
<Grid Visibility="{Binding Items.Count, Converter={StaticResource CountToVisibilityConverter}}" />
|
||||
<TextBlock Visibility="{Binding Items.Count, Converter={StaticResource CountToVisibilityConverter}, ConverterParameter=True}" />
|
||||
```
|
||||
|
||||
### Common Control Replacements
|
||||
|
||||
```xml
|
||||
|
||||
Reference in New Issue
Block a user