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:
moooyo
2026-07-31 16:28:38 +08:00
committed by GitHub
parent 8f63402400
commit 160492abcf
5 changed files with 86 additions and 2 deletions

View File

@@ -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` |

View File

@@ -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` |

View File

@@ -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`

View File

@@ -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` |

View File

@@ -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