[Skills] Add WPF to WinUI 3 migration agent skill (#46462)

<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
With this skills, we can easily enable AI to complete most of the tasks
involved in migrating from WPF to WinUI3.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [x] Closes: #46464
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

---------

Co-authored-by: Yu Leng <yuleng@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
moooyo
2026-03-24 17:40:33 +08:00
committed by GitHub
parent 0d41d45a64
commit 7051b8939b
7 changed files with 1878 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
---
name: wpf-to-winui3-migration
description: Guide for migrating PowerToys modules from WPF to WinUI 3 (Windows App SDK). Use when asked to migrate WPF code, convert WPF XAML to WinUI, replace System.Windows namespaces with Microsoft.UI.Xaml, update Dispatcher to DispatcherQueue, replace DynamicResource with ThemeResource, migrate imaging APIs from System.Windows.Media.Imaging to Windows.Graphics.Imaging, convert WPF Window to WinUI Window, migrate .resx to .resw resources, migrate custom Observable/RelayCommand to CommunityToolkit.Mvvm source generators, handle WPF-UI (Lepo) to WinUI native control migration, or fix installer/build pipeline issues after migration. Keywords: WPF, WinUI, WinUI3, migration, porting, convert, namespace, XAML, Dispatcher, DispatcherQueue, imaging, BitmapImage, Window, ContentDialog, ThemeResource, DynamicResource, ResourceLoader, resw, resx, CommunityToolkit, ObservableProperty, WPF-UI, SizeToContent, AppWindow, SoftwareBitmap.
license: Complete terms in LICENSE.txt
---
# WPF to WinUI 3 Migration Skill
Migrate PowerToys modules from WPF (`System.Windows.*`) to WinUI 3 (`Microsoft.UI.Xaml.*` / Windows App SDK). Based on patterns validated in the ImageResizer module migration.
## When to Use This Skill
- Migrate a PowerToys module from WPF to WinUI 3
- Convert WPF XAML files to WinUI 3 XAML
- Replace `System.Windows` namespaces with `Microsoft.UI.Xaml`
- Migrate `Dispatcher` usage to `DispatcherQueue`
- Migrate custom `Observable`/`RelayCommand` to CommunityToolkit.Mvvm source generators
- Replace WPF-UI (Lepo) controls with native WinUI 3 controls
- Convert imaging code from `System.Windows.Media.Imaging` to `Windows.Graphics.Imaging`
- Handle WPF `Window` vs WinUI `Window` differences (sizing, positioning, SizeToContent)
- Migrate resource files from `.resx` to `.resw` with `ResourceLoader`
- Fix installer/build pipeline issues after WinUI 3 migration
- Update project files, NuGet packages, and signing config
## Prerequisites
- Visual Studio 2022 17.4+
- Windows App SDK NuGet package (`Microsoft.WindowsAppSDK`)
- .NET 8+ with `net8.0-windows10.0.19041.0` TFM
- Windows 10 1803+ (April 2018 Update or newer)
## Migration Strategy
### Recommended Order
1. **Project file** — Update TFM, NuGet packages, set `<UseWinUI>true</UseWinUI>`
2. **Data models and business logic** — No UI dependencies, migrate first
3. **MVVM framework** — Replace custom Observable/RelayCommand with CommunityToolkit.Mvvm
4. **Resource strings** — Migrate `.resx``.resw`, introduce `ResourceLoaderInstance`
5. **Services and utilities** — Replace `System.Windows` types, async-ify imaging code
6. **ViewModels** — Update Dispatcher usage, binding patterns
7. **Views/Pages** — Starting from leaf pages with fewest dependencies
8. **Main page / shell** — Last, since it depends on everything
9. **App.xaml / startup code** — Merge carefully (do NOT overwrite WinUI 3 boilerplate)
10. **Installer & build pipeline** — Update WiX, signing, build events
11. **Tests** — Adapt for WinUI 3 runtime, async patterns
### Key Principles
- **Do NOT overwrite `App.xaml` / `App.xaml.cs`** — WinUI 3 has different application lifecycle boilerplate. Merge your resources and initialization code into the generated WinUI 3 App class.
- **Do NOT create Exe→WinExe `ProjectReference`** — Extract shared code to a Library project. This causes phantom build artifacts.
- **Use `Lazy<T>` for resource-dependent statics** — `ResourceLoader` is not available at class-load time in all contexts.
## Quick Reference Tables
### Namespace Mapping
| WPF | WinUI 3 |
|-----|---------|
| `System.Windows` | `Microsoft.UI.Xaml` |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` (UI) / `Windows.Graphics.Imaging` (processing) |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
| `System.Windows.Interop` | `WinRT.Interop` |
### Critical API Replacements
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `Dispatcher.Invoke()` | `DispatcherQueue.TryEnqueue()` | Different return type (`bool`) |
| `Dispatcher.CheckAccess()` | `DispatcherQueue.HasThreadAccess` | Property vs method |
| `Application.Current.Dispatcher` | Store `DispatcherQueue` in static field | See [Threading](./references/threading-and-windowing.md) |
| `MessageBox.Show()` | `ContentDialog` | Must set `XamlRoot` |
| `DynamicResource` | `ThemeResource` | Theme-reactive only |
| `clr-namespace:` | `using:` | XAML namespace prefix |
| `{x:Static props:Resources.Key}` | `x:Uid` or `ResourceLoader.GetString()` | .resx → .resw |
| `DataType="{x:Type m:Foo}"` | Remove or use code-behind | `x:Type` not supported |
| `Properties.Resources.MyString` | `ResourceLoaderInstance.ResourceLoader.GetString("MyString")` | Lazy-init pattern |
| `Application.Current.MainWindow` | Custom `App.Window` static property | Must track manually |
| `SizeToContent="Height"` | Custom `SizeToContent()` via `AppWindow.Resize()` | See [Windowing](./references/threading-and-windowing.md) |
| `MouseLeftButtonDown` | `PointerPressed` | Mouse → Pointer events |
| `Pack URI (pack://...)` | `ms-appx:///` | Resource URI scheme |
| `Observable` (custom base) | `ObservableObject` + `[ObservableProperty]` | CommunityToolkit.Mvvm |
| `RelayCommand` (custom) | `[RelayCommand]` source generator | CommunityToolkit.Mvvm |
| `JpegBitmapEncoder` | `BitmapEncoder.CreateAsync(JpegEncoderId, stream)` | Async, unified API |
| `encoder.QualityLevel = 85` | `BitmapPropertySet { "ImageQuality", 0.85f }` | int 1-100 → float 0-1 |
### NuGet Package Migration
| WPF | WinUI 3 |
|-----|---------|
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` |
| `WPF-UI` (Lepo) | Remove — use native WinUI 3 controls |
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) |
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` |
| (none) | `Microsoft.WindowsAppSDK` |
| (none) | `Microsoft.Windows.SDK.BuildTools` |
| (none) | `WinUIEx` (optional, for window helpers) |
| (none) | `CommunityToolkit.WinUI.Converters` |
### XAML Syntax Changes
| WPF | WinUI 3 |
|-----|---------|
| `xmlns:local="clr-namespace:MyApp"` | `xmlns:local="using:MyApp"` |
| `{DynamicResource Key}` | `{ThemeResource Key}` |
| `{x:Static Type.Member}` | `{x:Bind}` or code-behind |
| `{x:Type local:MyType}` | Not supported |
| `<Style.Triggers>` / `<DataTrigger>` | `VisualStateManager` |
| `{Binding}` in `Setter.Value` | Not supported — use `StaticResource` |
| `Content="{x:Static p:Resources.Cancel}"` | `x:Uid="Cancel"` with `.Content` in `.resw` |
| `<ui:FluentWindow>` / `<ui:Button>` (WPF-UI) | Native `<Window>` / `<Button>` |
| `<ui:NumberBox>` / `<ui:ProgressRing>` (WPF-UI) | Native `<NumberBox>` / `<ProgressRing>` |
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` |
| `IsDefault="True"` / `IsCancel="True"` | `Style="{StaticResource AccentButtonStyle}"` / handle via KeyDown |
| `<AccessText>` | Not available — use `AccessKey` property |
| `<behaviors:Interaction.Triggers>` | Migrate to code-behind or WinUI behaviors |
## Detailed Reference Docs
Read only the section relevant to your current task:
- [Namespace and API Mapping](./references/namespace-api-mapping.md) — Full type mapping, NuGet changes, project file, CsWinRT interop
- [XAML Migration Guide](./references/xaml-migration.md) — XAML syntax, WPF-UI removal, markup extensions, styles, resources, data binding
- [Threading and Window Management](./references/threading-and-windowing.md) — Dispatcher, DispatcherQueue, SizeToContent, AppWindow, HWND interop, custom entry point
- [Imaging API Migration](./references/imaging-migration.md) — BitmapEncoder/Decoder, SoftwareBitmap, CodecHelper, async patterns, int→uint
- [PowerToys-Specific Patterns](./references/powertoys-patterns.md) — MVVM migration, ResourceLoader, Lazy init, installer, signing, test adaptation, build pipeline
## Common Pitfalls (from ImageResizer migration)
| Pitfall | Solution |
|---------|----------|
| `ContentDialog` throws "does not have a XamlRoot" | Set `dialog.XamlRoot = this.Content.XamlRoot` before `ShowAsync()` |
| `FilePicker` throws error in desktop app | Call `WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd)` |
| `Window.Dispatcher` returns null | Use `Window.DispatcherQueue` instead |
| Resources on `Window` element not found | Move resources to root layout container (`Grid.Resources`) |
| `VisualStateManager` on `Window` fails | Use `UserControl` or `Page` inside the Window |
| Satellite assembly installer errors (`WIX0103`) | Remove `.resources.dll` refs from `Resources.wxs`; WinUI 3 uses `.pri` |
| Phantom `.exe`/`.deps.json` in root output dir | Avoid Exe→WinExe `ProjectReference`; use Library project |
| `ResourceLoader` crash at static init | Wrap in `Lazy<T>` or null-coalescing property — see [Lazy Init](./references/powertoys-patterns.md#lazy-initialization-for-resource-dependent-statics) |
| `SizeToContent` not available | Implement manual content measurement + `AppWindow.Resize()` with DPI scaling |
| `x:Bind` default mode is `OneTime` | Explicitly set `Mode=OneWay` or `Mode=TwoWay` |
| `DynamicResource` / `x:Static` not compiling | Replace with `ThemeResource` / `ResourceLoader` or `x:Uid` |
| `IValueConverter.Convert` signature mismatch | Last param: `CultureInfo``string` (language tag) |
| Test project can't resolve WPF types | Add `<UseWPF>true</UseWPF>` temporarily; remove after imaging migration |
| Pixel dimension type mismatch (`int` vs `uint`) | WinRT uses `uint` for pixel sizes — add `u` suffix in test assertions |
| `$(SolutionDir)` empty in standalone project build | Use `$(MSBuildThisFileDirectory)` with relative paths instead |
| JPEG quality value wrong after migration | WPF: int 1-100; WinRT: float 0.0-1.0 |
| MSIX packaging fails in PreBuildEvent | Move to PostBuildEvent; artifacts not ready at PreBuild time |
| RC file icon path with forward slashes | Use double-backslash escaping: `..\\ui\\Assets\\icon.ico` |
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Build fails after namespace rename | Check for lingering `System.Windows` usings; some types have no direct equivalent |
| Missing `PresentationCore.dll` at runtime | Ensure ALL imaging code uses `Windows.Graphics.Imaging`, not `System.Windows.Media.Imaging` |
| `DataContext` not working on Window | WinUI 3 `Window` is not a `DependencyObject`; use a root `Page` or `UserControl` |
| XAML designer not available | WinUI 3 does not support XAML Designer; use Hot Reload instead |
| NuGet restore failures | Run `build-essentials.cmd` after adding `Microsoft.WindowsAppSDK` package |
| `Parallel.ForEach` compilation error | Migrate to `Parallel.ForEachAsync` for async imaging operations |
| Signing check fails on leaked artifacts | Run `generateAllFileComponents.ps1`; verify only `WinUI3Apps\\` paths in signing config |

View File

@@ -0,0 +1,287 @@
# Imaging API Migration
Migrating from WPF (`System.Windows.Media.Imaging` / `PresentationCore.dll`) to WinRT (`Windows.Graphics.Imaging`). Based on the ImageResizer migration.
## Why This Migration Is Required
WinUI 3 apps deployed as self-contained do NOT include `PresentationCore.dll`. Any code using `System.Windows.Media.Imaging` will throw `FileNotFoundException` at runtime. ALL imaging code must use WinRT APIs.
| Purpose | Namespace |
|---------|-----------|
| UI display (`Image.Source`) | `Microsoft.UI.Xaml.Media.Imaging` |
| Image processing (encode/decode/transform) | `Windows.Graphics.Imaging` |
## Architecture Change: Pipeline vs Declarative
The fundamental architecture differs:
**WPF**: In-memory pipeline of bitmap objects. Decode → transform → encode synchronously.
```csharp
var decoder = BitmapDecoder.Create(stream, ...);
var transform = new TransformedBitmap(decoder.Frames[0], new ScaleTransform(...));
var encoder = new JpegBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(transform, ...));
encoder.Save(outputStream);
```
**WinRT**: Declarative transform model. Configure transforms on the encoder, which handles pixel manipulation internally. All async.
```csharp
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
encoder.BitmapTransform.ScaledWidth = newWidth;
encoder.BitmapTransform.ScaledHeight = newHeight;
encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
await encoder.FlushAsync();
```
## Core Type Mapping
### Decoders
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapDecoder.Create(stream, options, cache)` | `BitmapDecoder.CreateAsync(stream)` | Async, auto-detects format |
| `JpegBitmapDecoder` / `PngBitmapDecoder` / etc. | `BitmapDecoder.CreateAsync(stream)` | Single unified decoder |
| `decoder.Frames[0]` | `await decoder.GetFrameAsync(0)` | Async frame access |
| `decoder.Frames.Count` | `decoder.FrameCount` (uint) | `int``uint` |
| `decoder.CodecInfo.ContainerFormat` | `decoder.DecoderInformation.CodecId` | Different property path |
| `decoder.Frames[0].PixelWidth` (int) | `decoder.PixelWidth` (uint) | `int``uint` |
| `WmpBitmapDecoder` | Not available | WMP/HDP not supported |
### Encoders
| WPF | WinRT | Notes |
|-----|-------|-------|
| `new JpegBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream)` | Async factory |
| `new PngBitmapEncoder()` | `BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream)` | No interlace control |
| `encoder.Frames.Add(frame)` | `encoder.SetSoftwareBitmap(bitmap)` | Different API |
| `encoder.Save(stream)` | `await encoder.FlushAsync()` | Async |
### Encoder Properties (Strongly-Typed → BitmapPropertySet)
WPF had type-specific encoder subclasses. WinRT uses a generic property set:
```csharp
// WPF
case JpegBitmapEncoder jpeg: jpeg.QualityLevel = 85; // int 1-100
case PngBitmapEncoder png: png.Interlace = PngInterlaceOption.On;
case TiffBitmapEncoder tiff: tiff.Compression = TiffCompressOption.Lzw;
// WinRT — JPEG quality (float 0.0-1.0)
await encoder.BitmapProperties.SetPropertiesAsync(new BitmapPropertySet
{
{ "ImageQuality", new BitmapTypedValue(0.85f, PropertyType.Single) }
});
// WinRT — TIFF compression (via BitmapPropertySet at creation time)
var props = new BitmapPropertySet
{
{ "TiffCompressionMethod", new BitmapTypedValue((byte)2, PropertyType.UInt8) }
};
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.TiffEncoderId, stream, props);
```
**JPEG quality scale change**: WPF int `1-100` → WinRT float `0.0-1.0`. Divide by 100.
### Bitmap Types
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapSource` | `SoftwareBitmap` | Central pixel-data type |
| `BitmapImage` | `BitmapImage` (in `Microsoft.UI.Xaml.Media.Imaging`) | UI display only |
| `FormatConvertedBitmap` | `SoftwareBitmap.Convert()` | |
| `TransformedBitmap` + `ScaleTransform` | `BitmapTransform` via encoder | Declarative |
| `CroppedBitmap` | `BitmapTransform.Bounds` | |
### Metadata
| WPF | WinRT | Notes |
|-----|-------|-------|
| `BitmapMetadata` | `BitmapProperties` | Different API surface |
| `BitmapMetadata.Clone()` | No equivalent | Cannot selectively clone |
| Selective metadata removal | Not supported | All-or-nothing only |
**Two encoder creation strategies for metadata:**
- `CreateForTranscodingAsync()` — preserves ALL metadata from source
- `CreateAsync()` — creates fresh encoder with NO metadata
This eliminated ~258 lines of manual metadata manipulation code (`BitmapMetadataExtension.cs`) in ImageResizer.
### Interpolation Modes
| WPF `BitmapScalingMode` | WinRT `BitmapInterpolationMode` |
|------------------------|-------------------------------|
| `HighQuality` / `Fant` | `Fant` |
| `Linear` | `Linear` |
| `NearestNeighbor` | `NearestNeighbor` |
| `Unspecified` / `LowQuality` | `Linear` |
## Stream Interop
WinRT imaging requires `IRandomAccessStream` instead of `System.IO.Stream`:
```csharp
using var stream = File.OpenRead(path);
var winrtStream = stream.AsRandomAccessStream(); // Extension method
var decoder = await BitmapDecoder.CreateAsync(winrtStream);
```
**Critical**: For transcode, seek the input stream back to 0 before creating the encoder:
```csharp
winrtStream.Seek(0);
var encoder = await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);
```
## CodecHelper Pattern (from ImageResizer)
WPF stored container format GUIDs in `settings.json`. WinRT uses different codec IDs. Create a `CodecHelper` to bridge them:
```csharp
internal static class CodecHelper
{
// Maps WPF container format GUIDs (stored in settings JSON) to WinRT encoder IDs
private static readonly Dictionary<Guid, Guid> LegacyGuidToEncoderId = new()
{
[new Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057")] = BitmapEncoder.JpegEncoderId,
[new Guid("1b7cfaf4-713f-473c-bbcd-6137425faeaf")] = BitmapEncoder.PngEncoderId,
[new Guid("0af1d87e-fcfe-4188-bdeb-a7906471cbe3")] = BitmapEncoder.BmpEncoderId,
[new Guid("163bcc30-e2e9-4f0b-961d-a3e9fdb788a3")] = BitmapEncoder.TiffEncoderId,
[new Guid("1f8a5601-7d4d-4cbd-9c82-1bc8d4eeb9a5")] = BitmapEncoder.GifEncoderId,
};
// Maps decoder IDs to corresponding encoder IDs
private static readonly Dictionary<Guid, Guid> DecoderIdToEncoderId = new()
{
[BitmapDecoder.JpegDecoderId] = BitmapEncoder.JpegEncoderId,
[BitmapDecoder.PngDecoderId] = BitmapEncoder.PngEncoderId,
// ...
};
public static Guid GetEncoderIdFromLegacyGuid(Guid legacyGuid)
=> LegacyGuidToEncoderId.GetValueOrDefault(legacyGuid, Guid.Empty);
public static Guid GetEncoderIdForDecoder(BitmapDecoder decoder)
=> DecoderIdToEncoderId.GetValueOrDefault(decoder.DecoderInformation.CodecId, Guid.Empty);
}
```
This preserves backward compatibility with existing `settings.json` files that contain WPF-era GUIDs.
## ImagingEnums Pattern (from ImageResizer)
WPF-specific enums (`PngInterlaceOption`, `TiffCompressOption`) from `System.Windows.Media.Imaging` are used in settings JSON. Create custom enums with identical integer values for backward-compatible deserialization:
```csharp
// Replace System.Windows.Media.Imaging.PngInterlaceOption
public enum PngInterlaceOption { Default = 0, On = 1, Off = 2 }
// Replace System.Windows.Media.Imaging.TiffCompressOption
public enum TiffCompressOption { Default = 0, None = 1, Ccitt3 = 2, Ccitt4 = 3, Lzw = 4, Rle = 5, Zip = 6 }
```
## Async Migration Patterns
### Method Signatures
All imaging operations become async:
| Before | After |
|--------|-------|
| `void Execute(file, settings)` | `async Task ExecuteAsync(file, settings)` |
| `IEnumerable<Error> Process()` | `async Task<IEnumerable<Error>> ProcessAsync()` |
### Parallel Processing
```csharp
// WPF (synchronous)
Parallel.ForEach(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
(file, state, i) => { Execute(file, settings); });
// WinRT (async)
await Parallel.ForEachAsync(Files, new ParallelOptions { MaxDegreeOfParallelism = ... },
async (file, ct) => { await ExecuteAsync(file, settings); });
```
### CLI Async Bridge
CLI entry points must bridge async to sync:
```csharp
return RunSilentModeAsync(cliOptions).GetAwaiter().GetResult();
```
### Task.Factory.StartNew → Task.Run
```csharp
// WPF
_ = Task.Factory.StartNew(StartExecutingWork, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
// WinUI 3
_ = Task.Run(() => StartExecutingWorkAsync());
```
## SoftwareBitmap as Interface Type
When modules expose imaging interfaces (e.g., AI super-resolution), change parameter/return types:
```csharp
// WPF
BitmapSource ApplySuperResolution(BitmapSource source, int scale, string filePath);
// WinRT
SoftwareBitmap ApplySuperResolution(SoftwareBitmap source, int scale, string filePath);
```
This eliminates manual `BitmapSource ↔ SoftwareBitmap` conversion code (unsafe `IMemoryBufferByteAccess` COM interop).
## MultiFrame Image Handling
```csharp
// WinRT multi-frame encode (e.g., multi-page TIFF, animated GIF)
for (uint i = 0; i < decoder.FrameCount; i++)
{
if (i > 0)
await encoder.GoToNextFrameAsync();
var frame = await decoder.GetFrameAsync(i);
var bitmap = await frame.GetSoftwareBitmapAsync(
frame.BitmapPixelFormat,
BitmapAlphaMode.Premultiplied,
transform,
ExifOrientationMode.IgnoreExifOrientation,
ColorManagementMode.DoNotColorManage);
encoder.SetSoftwareBitmap(bitmap);
}
await encoder.FlushAsync();
```
## int → uint for Pixel Dimensions
WinRT uses `uint` for all pixel dimensions. This affects:
- `decoder.PixelWidth` / `decoder.PixelHeight``uint`
- `BitmapTransform.ScaledWidth` / `ScaledHeight``uint`
- `SoftwareBitmap` constructor — `uint` parameters
- Test assertions: `Assert.AreEqual(96, ...)``Assert.AreEqual(96u, ...)`
## Display SoftwareBitmap in UI
```csharp
var source = new SoftwareBitmapSource();
// Must convert to Bgra8/Premultiplied for display
if (bitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
bitmap.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
{
bitmap = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}
await source.SetBitmapAsync(bitmap);
myImage.Source = source;
```
## Known Limitations
| Feature | WPF | WinRT | Impact |
|---------|-----|-------|--------|
| PNG interlace | `PngBitmapEncoder.Interlace` | Not available | Always non-interlaced |
| Metadata stripping | Selective via `BitmapMetadata.Clone()` | All-or-nothing | Orientation EXIF also removed |
| Pixel formats | Many (`Pbgra32`, `Bgr24`, `Indexed8`, ...) | Primarily `Bgra8`, `Rgba8`, `Gray8/16` | Convert to `Bgra8` |
| WMP/HDP format | `WmpBitmapDecoder` | Not available | Not supported |
| Pixel differences | WPF scaler | `BitmapInterpolationMode.Fant` | Not bit-identical |

View File

@@ -0,0 +1,226 @@
# Namespace and API Mapping Reference
Complete reference for mapping WPF types to WinUI 3 equivalents, based on the ImageResizer migration.
## Root Namespace Mapping
| WPF Namespace | WinUI 3 Namespace |
|---------------|-------------------|
| `System.Windows` | `Microsoft.UI.Xaml` |
| `System.Windows.Automation` | `Microsoft.UI.Xaml.Automation` |
| `System.Windows.Automation.Peers` | `Microsoft.UI.Xaml.Automation.Peers` |
| `System.Windows.Controls` | `Microsoft.UI.Xaml.Controls` |
| `System.Windows.Controls.Primitives` | `Microsoft.UI.Xaml.Controls.Primitives` |
| `System.Windows.Data` | `Microsoft.UI.Xaml.Data` |
| `System.Windows.Documents` | `Microsoft.UI.Xaml.Documents` |
| `System.Windows.Input` | `Microsoft.UI.Xaml.Input` |
| `System.Windows.Markup` | `Microsoft.UI.Xaml.Markup` |
| `System.Windows.Media` | `Microsoft.UI.Xaml.Media` |
| `System.Windows.Media.Animation` | `Microsoft.UI.Xaml.Media.Animation` |
| `System.Windows.Media.Imaging` | `Microsoft.UI.Xaml.Media.Imaging` |
| `System.Windows.Navigation` | `Microsoft.UI.Xaml.Navigation` |
| `System.Windows.Shapes` | `Microsoft.UI.Xaml.Shapes` |
| `System.Windows.Threading` | `Microsoft.UI.Dispatching` |
| `System.Windows.Interop` | `WinRT.Interop` |
## Core Type Mapping
| WPF Type | WinUI 3 Type |
|----------|-------------|
| `System.Windows.Application` | `Microsoft.UI.Xaml.Application` |
| `System.Windows.Window` | `Microsoft.UI.Xaml.Window` (NOT a DependencyObject) |
| `System.Windows.DependencyObject` | `Microsoft.UI.Xaml.DependencyObject` |
| `System.Windows.DependencyProperty` | `Microsoft.UI.Xaml.DependencyProperty` |
| `System.Windows.FrameworkElement` | `Microsoft.UI.Xaml.FrameworkElement` |
| `System.Windows.UIElement` | `Microsoft.UI.Xaml.UIElement` |
| `System.Windows.Visibility` | `Microsoft.UI.Xaml.Visibility` |
| `System.Windows.Thickness` | `Microsoft.UI.Xaml.Thickness` |
| `System.Windows.CornerRadius` | `Microsoft.UI.Xaml.CornerRadius` |
| `System.Windows.Media.Color` | `Windows.UI.Color` (note: `Windows.UI`, not `Microsoft.UI`) |
| `System.Windows.Media.Colors` | `Microsoft.UI.Colors` |
## Controls Mapping
### Direct Mapping (namespace-only change)
These controls exist in both frameworks with the same name — change `System.Windows.Controls` to `Microsoft.UI.Xaml.Controls`:
`Button`, `TextBox`, `TextBlock`, `ComboBox`, `CheckBox`, `ListBox`, `ListView`, `Image`, `StackPanel`, `Grid`, `Border`, `ScrollViewer`, `ContentControl`, `UserControl`, `Page`, `Frame`, `Slider`, `ProgressBar`, `ToolTip`, `RadioButton`, `ToggleButton`
### Controls With Different Names or Behavior
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `MessageBox` | `ContentDialog` | Must set `XamlRoot` before `ShowAsync()` |
| `ContextMenu` | `MenuFlyout` | Different API surface |
| `TabControl` | `TabView` | Different API |
| `Menu` | `MenuBar` | Different API |
| `StatusBar` | Custom `StackPanel` layout | No built-in equivalent |
| `AccessText` | Not available | Use `AccessKey` property on target control |
### WPF-UI (Lepo) to Native WinUI 3
ImageResizer used the `WPF-UI` library (Lepo) for Fluent styling. These must be replaced with native WinUI 3 equivalents:
| WPF-UI (Lepo) | WinUI 3 Native | Notes |
|----------------|---------------|-------|
| `<ui:FluentWindow>` | `<Window>` | Native window + `ExtendsContentIntoTitleBar` |
| `<ui:Button>` | `<Button>` | Native button |
| `<ui:NumberBox>` | `<NumberBox>` | Built into WinUI 3 |
| `<ui:ProgressRing>` | `<ProgressRing>` | Built into WinUI 3 |
| `<ui:SymbolIcon>` | `<SymbolIcon>` or `<FontIcon>` | Built into WinUI 3 |
| `<ui:InfoBar>` | `<InfoBar>` | Built into WinUI 3 |
| `<ui:TitleBar>` | Custom title bar via `SetTitleBar()` | Use `ExtendsContentIntoTitleBar` |
| `<ui:ThemesDictionary>` | `<XamlControlsResources>` | In merged dictionaries |
| `<ui:ControlsDictionary>` | Remove | Not needed — WinUI 3 has its own control styles |
| `BasedOn="{StaticResource {x:Type ui:Button}}"` | `BasedOn="{StaticResource DefaultButtonStyle}"` | Named style keys |
## Input Event Mapping
| WPF Event | WinUI 3 Event | Notes |
|-----------|--------------|-------|
| `MouseLeftButtonDown` | `PointerPressed` | Check `IsLeftButtonPressed` on args |
| `MouseLeftButtonUp` | `PointerReleased` | Check pointer properties |
| `MouseRightButtonDown` | `RightTapped` | Or `PointerPressed` with right button check |
| `MouseMove` | `PointerMoved` | Uses `PointerRoutedEventArgs` |
| `MouseWheel` | `PointerWheelChanged` | Different event args |
| `MouseEnter` | `PointerEntered` | |
| `MouseLeave` | `PointerExited` | |
| `MouseDoubleClick` | `DoubleTapped` | Different event args |
| `KeyDown` | `KeyDown` | Same name, args type: `KeyRoutedEventArgs` |
| `PreviewKeyDown` | No direct equivalent | Use `KeyDown` with handled pattern |
## IValueConverter Signature Change
| WPF | WinUI 3 |
|-----|---------|
| `Convert(object value, Type targetType, object parameter, CultureInfo culture)` | `Convert(object value, Type targetType, object parameter, string language)` |
| `ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)` | `ConvertBack(object value, Type targetType, object parameter, string language)` |
Last parameter changes from `CultureInfo` to `string` (BCP-47 language tag). All converter classes must be updated.
## Types That Moved to Different Hierarchies
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| `System.Windows.Threading.Dispatcher` | `Microsoft.UI.Dispatching.DispatcherQueue` | Completely different API |
| `System.Windows.Threading.DispatcherPriority` | `Microsoft.UI.Dispatching.DispatcherQueuePriority` | Only 3 levels: High/Normal/Low |
| `System.Windows.Interop.HwndSource` | `WinRT.Interop.WindowNative` | For HWND interop |
| `System.Windows.Interop.WindowInteropHelper` | `WinRT.Interop.WindowNative.GetWindowHandle()` | |
| `System.Windows.SystemColors` | Resource keys via `ThemeResource` | No direct static class |
| `System.Windows.SystemParameters` | Win32 API or `DisplayInformation` | No direct equivalent |
## NuGet Package Migration
| WPF | WinUI 3 | Notes |
|-----|---------|-------|
| Built into .NET (no NuGet needed) | `Microsoft.WindowsAppSDK` | Required |
| `PresentationCore` / `PresentationFramework` | `Microsoft.WinUI` (transitive) | |
| `Microsoft.Xaml.Behaviors.Wpf` | `Microsoft.Xaml.Behaviors.WinUI.Managed` | |
| `WPF-UI` (Lepo) | **Remove** — use native WinUI 3 controls | |
| `CommunityToolkit.Mvvm` | `CommunityToolkit.Mvvm` (same) | |
| `Microsoft.Toolkit.Wpf.*` | `CommunityToolkit.WinUI.*` | |
| (none) | `Microsoft.Windows.SDK.BuildTools` | Required |
| (none) | `WinUIEx` | Optional, window helpers |
| (none) | `CommunityToolkit.WinUI.Converters` | Optional |
| (none) | `CommunityToolkit.WinUI.Extensions` | Optional |
| (none) | `Microsoft.Web.WebView2` | If using WebView |
## Project File Changes
### WPF .csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
</Project>
```
### WinUI 3 .csproj
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<UseWinUI>true</UseWinUI>
<SelfContained>true</SelfContained>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsPackageType>None</WindowsPackageType>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
<ProjectPriFileName>PowerToys.ModuleName.pri</ProjectPriFileName>
</PropertyGroup>
</Project>
```
Key changes:
- `UseWPF``UseWinUI`
- TFM: `net8.0-windows``net8.0-windows10.0.19041.0`
- Add `WindowsPackageType=None` for unpackaged desktop apps
- Add `SelfContained=true` + `WindowsAppSDKSelfContained=true`
- Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs` entry point
- Set `ProjectPriFileName` to match your module's assembly name
- Move icon from `Resources/` to `Assets/<Module>/`
### XAML ApplicationDefinition Setup
WinUI 3 requires explicit `ApplicationDefinition` declaration:
```xml
<ItemGroup>
<Page Remove="ImageResizerXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
</ItemGroup>
```
### CsWinRT Interop (for GPO and native references)
If the module references native C++ projects (like `GPOWrapper`):
```xml
<PropertyGroup>
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
```
Change `GPOWrapperProjection.csproj` reference to direct `GPOWrapper.vcxproj` reference.
### InternalsVisibleTo Migration
Move from code file to `.csproj`:
```csharp
// DELETE: Properties/InternalsVisibleTo.cs
// [assembly: InternalsVisibleTo("ImageResizer.Test")]
```
```xml
<!-- ADD to .csproj: -->
<ItemGroup>
<InternalsVisibleTo Include="ImageResizer.Test" />
</ItemGroup>
```
### Items to Remove from .csproj
```xml
<!-- DELETE: WPF resource embedding -->
<EmbeddedResource Update="Properties\Resources.resx">...</EmbeddedResource>
<Resource Include="Resources\ImageResizer.ico" />
<Compile Update="Properties\Resources.Designer.cs">...</Compile>
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" /> <!-- from CLI project -->
```

View File

@@ -0,0 +1,516 @@
# PowerToys-Specific Migration Patterns
Patterns and conventions specific to the PowerToys codebase, based on the ImageResizer migration.
## Project Structure
### Before (WPF Module)
```
src/modules/<module>/
├── <Module>UI/
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWPF=true
│ ├── App.xaml / App.xaml.cs
│ ├── MainWindow.xaml / .cs
│ ├── Views/
│ ├── ViewModels/
│ ├── Helpers/
│ │ ├── Observable.cs # Custom INotifyPropertyChanged
│ │ └── RelayCommand.cs # Custom ICommand
│ ├── Properties/
│ │ ├── Resources.resx # WPF resource strings
│ │ ├── Resources.Designer.cs
│ │ └── InternalsVisibleTo.cs
│ └── Telemetry/
├── <Module>CLI/
│ └── <Module>CLI.csproj # OutputType=Exe
└── tests/
```
### After (WinUI 3 Module)
```
src/modules/<module>/
├── <Module>UI/
│ ├── <Module>UI.csproj # OutputType=WinExe, UseWinUI=true
│ ├── Program.cs # Custom entry point (DISABLE_XAML_GENERATED_MAIN)
│ ├── app.manifest # Single manifest file
│ ├── ImageResizerXAML/
│ │ ├── App.xaml / App.xaml.cs # WinUI 3 App class
│ │ ├── MainWindow.xaml / .cs
│ │ └── Views/
│ ├── Converters/ # WinUI 3 IValueConverter (string language)
│ ├── ViewModels/
│ ├── Helpers/
│ │ └── ResourceLoaderInstance.cs # Static ResourceLoader accessor
│ ├── Utilities/
│ │ └── CodecHelper.cs # WPF→WinRT codec ID mapping (if imaging)
│ ├── Models/
│ │ └── ImagingEnums.cs # Custom enums replacing WPF imaging enums
│ ├── Strings/
│ │ └── en-us/
│ │ └── Resources.resw # WinUI 3 resource strings
│ └── Assets/
│ └── <Module>/
│ └── <Module>.ico # Moved from Resources/
├── <Module>Common/ # NEW: shared library for CLI
│ └── <Module>Common.csproj # OutputType=Library
├── <Module>CLI/
│ └── <Module>CLI.csproj # References Common, NOT UI
└── tests/
```
### Critical: CLI Dependency Pattern
**Do NOT** create `ProjectReference` from Exe to WinExe. This causes phantom build artifacts (`.exe`, `.deps.json`, `.runtimeconfig.json`) in the root output directory.
```
WRONG: ImageResizerCLI (Exe) → ImageResizerUI (WinExe) ← phantom artifacts
CORRECT: ImageResizerCLI (Exe) → ImageResizerCommon (Library)
ImageResizerUI (WinExe) → ImageResizerCommon (Library)
```
Follow the `FancyZonesCLI``FancyZonesEditorCommon` pattern.
### Files to Delete
| File | Reason |
|------|--------|
| `Properties/Resources.resx` | Replaced by `Strings/en-us/Resources.resw` |
| `Properties/Resources.Designer.cs` | Auto-generated; no longer needed |
| `Properties/InternalsVisibleTo.cs` | Moved to `.csproj` `<InternalsVisibleTo>` |
| `Helpers/Observable.cs` | Replaced by `CommunityToolkit.Mvvm.ObservableObject` |
| `Helpers/RelayCommand.cs` | Replaced by `CommunityToolkit.Mvvm.Input` |
| `Resources/*.ico` / `Resources/*.png` | Moved to `Assets/<Module>/` |
| WPF `.dev.manifest` / `.prod.manifest` | Replaced by single `app.manifest` |
| WPF-specific converters | Replaced by WinUI 3 converters with `string language` |
---
## MVVM Migration: Custom → CommunityToolkit.Mvvm Source Generators
### Observable Base Class → ObservableObject + [ObservableProperty]
**Before (custom Observable):**
```csharp
public class ResizeSize : Observable
{
private int _id;
public int Id { get => _id; set => Set(ref _id, value); }
private ResizeFit _fit;
public ResizeFit Fit
{
get => _fit;
set
{
Set(ref _fit, value);
UpdateShowHeight();
}
}
private bool _showHeight = true;
public bool ShowHeight { get => _showHeight; set => Set(ref _showHeight, value); }
private void UpdateShowHeight() { ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent; }
}
```
**After (CommunityToolkit.Mvvm source generators):**
```csharp
public partial class ResizeSize : ObservableObject // MUST be partial
{
[ObservableProperty]
[JsonPropertyName("Id")]
private int _id;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowHeight))] // Replaces manual UpdateShowHeight()
private ResizeFit _fit;
// Computed property — no backing field, no manual update method
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
}
```
Key changes:
- Class must be `partial` for source generators
- `Observable``ObservableObject` (from CommunityToolkit.Mvvm)
- Manual `Set(ref _field, value)``[ObservableProperty]` attribute
- `PropertyChanged` dependencies → `[NotifyPropertyChangedFor(nameof(...))]`
- Computed properties with manual `UpdateXxx()` → direct expression body
### Custom Name Setter with Transform
For properties that transform the value before storing:
```csharp
// Cannot use [ObservableProperty] because of value transformation
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, ReplaceTokens(value)); // SetProperty from ObservableObject
}
```
### RelayCommand → [RelayCommand] Source Generator
```csharp
// DELETE: Helpers/RelayCommand.cs (custom ICommand)
// Before
public ICommand ResizeCommand { get; } = new RelayCommand(Execute);
// After
[RelayCommand]
private void Resize() { /* ... */ }
// Source generator creates ResizeCommand property automatically
```
---
## Resource String Migration (.resx → .resw)
### ResourceLoaderInstance Helper
```csharp
internal static class ResourceLoaderInstance
{
internal static ResourceLoader ResourceLoader { get; private set; }
static ResourceLoaderInstance()
{
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
}
}
```
**Note**: Use the single-argument `ResourceLoader` constructor. The two-argument version (`ResourceLoader("file.pri", "path/Resources")`) may fail if the resource map path doesn't match the actual PRI structure.
### Usage
```csharp
// WPF
using ImageResizer.Properties;
string text = Resources.MyStringKey;
// WinUI 3
string text = ResourceLoaderInstance.ResourceLoader.GetString("MyStringKey");
```
### Lazy Initialization for Resource-Dependent Statics
`ResourceLoader` is not available at class-load time in all contexts (CLI mode, test harness). Use lazy initialization:
**Before (crashes at class load):**
```csharp
private static readonly CompositeFormat _format =
CompositeFormat.Parse(Resources.Error_Format);
private static readonly Dictionary<string, string> _tokens = new()
{
["$small$"] = Resources.Small,
["$medium$"] = Resources.Medium,
};
```
**After (lazy, safe):**
```csharp
private static CompositeFormat _format;
private static CompositeFormat Format => _format ??=
CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_Format"));
private static readonly Lazy<Dictionary<string, string>> _tokens = new(() =>
new Dictionary<string, string>
{
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
});
// Usage: _tokens.Value.TryGetValue(...)
```
### XAML: x:Static → x:Uid
```xml
<!-- WPF -->
<Button Content="{x:Static p:Resources.Cancel}" />
<!-- WinUI 3 -->
<Button x:Uid="Cancel" />
```
In `.resw`, use property-suffixed keys: `Cancel.Content`, `Header.Text`, etc.
---
## CLI Options Migration
`System.CommandLine.Option<T>` constructor signature changed:
```csharp
// WPF era — string[] aliases
public DestinationOption()
: base(_aliases, Properties.Resources.CLI_Option_Destination)
// WinUI 3 — single string name
public DestinationOption()
: base(_aliases[0], ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination"))
```
---
## Installer Updates
### WiX Changes
#### 1. Remove Satellite Assembly References
Remove from `installer/PowerToysSetupVNext/Resources.wxs`:
- `<Component>` entries for `<Module>.resources.dll`
- `<RemoveFolder>` entries for locale directories
- Module from `WinUI3AppsInstallFolder` `ParentDirectory` loop
#### 2. Update File Component Generation
Run `generateAllFileComponents.ps1` after migration. For Exe→WinExe dependency issues, add cleanup logic:
```powershell
# Strip phantom ImageResizer files from BaseApplications.wxs
$content = $content -replace 'PowerToys\.ImageResizer\.exe', ''
$content = $content -replace 'PowerToys\.ImageResizer\.deps\.json', ''
$content = $content -replace 'PowerToys\.ImageResizer\.runtimeconfig\.json', ''
```
#### 3. Output Directory
WinUI 3 modules output to `WinUI3Apps/`:
```xml
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
```
### ESRP Signing
Update `.pipelines/ESRPSigning_core.json` — all module binaries must use `WinUI3Apps\\` paths:
```json
{
"FileList": [
"WinUI3Apps\\PowerToys.ImageResizer.exe",
"WinUI3Apps\\PowerToys.ImageResizerExt.dll",
"WinUI3Apps\\PowerToys.ImageResizerContextMenu.dll"
]
}
```
---
## Build Pipeline Fixes
### $(SolutionDir) → $(MSBuildThisFileDirectory)
`$(SolutionDir)` is empty when building individual projects outside the solution. Replace with relative paths from the project file:
```xml
<!-- Before (breaks on standalone project build) -->
<Exec Command="powershell $(SolutionDir)tools\build\convert-resx-to-rc.ps1" />
<!-- After (works always) -->
<Exec Command="powershell $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1" />
```
### MSIX Packaging: PreBuild → PostBuild
MSIX packaging must happen AFTER the build (artifacts not ready at PreBuild):
```xml
<!-- Before -->
<PreBuildEvent>MakeAppx.exe pack /d . /p "$(OutDir)Package.msix" /o</PreBuildEvent>
<!-- After -->
<PostBuildEvent>
if exist "$(OutDir)Package.msix" del "$(OutDir)Package.msix"
MakeAppx.exe pack /d "$(MSBuildThisFileDirectory)." /p "$(OutDir)Package.msix" /o
</PostBuildEvent>
```
### RC File Icon Path Escaping
Windows Resource Compiler requires double-backslash paths:
```c
// Before (breaks)
IDI_ICON1 ICON "..\\ui\Assets\ImageResizer\ImageResizer.ico"
// After
IDI_ICON1 ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
```
### BOM/Encoding Normalization
Migration may strip UTF-8 BOM from C# files (`// Copyright``// Copyright`). This is cosmetic and safe, but be aware it will show as changes in diff.
---
## Test Adaptation
### Tests Requiring WPF Runtime
If tests still need WPF types (e.g., comparing old vs new output), temporarily add:
```xml
<UseWPF>true</UseWPF>
```
Remove this after fully migrating all test code to WinRT APIs.
### Tests Using ResourceLoader
Unit tests cannot easily initialize WinUI 3 `ResourceLoader`. Options:
- Hardcode expected strings in tests: `"Value must be between '{0}' and '{1}'."`
- Delete tests that only verify resource string lookup
- Avoid creating `App` instances in test harness (WinUI App cannot be instantiated in tests)
### Async Test Methods
All imaging tests become async:
```csharp
// Before
[TestMethod]
public void ResizesImage() { ... }
// After
[TestMethod]
public async Task ResizesImageAsync() { ... }
```
### uint Assertions
```csharp
// Before
Assert.AreEqual(96, image.Frames[0].PixelWidth);
// After
Assert.AreEqual(96u, decoder.PixelWidth);
```
### Pixel Data Access in Tests
```csharp
// Before (WPF)
public static Color GetFirstPixel(this BitmapSource source)
{
var pixel = new byte[4];
new FormatConvertedBitmap(
new CroppedBitmap(source, new Int32Rect(0, 0, 1, 1)),
PixelFormats.Bgra32, null, 0).CopyPixels(pixel, 4, 0);
return Color.FromArgb(pixel[3], pixel[2], pixel[1], pixel[0]);
}
// After (WinRT)
public static async Task<(byte R, byte G, byte B, byte A)> GetFirstPixelAsync(
this BitmapDecoder decoder)
{
using var bitmap = await decoder.GetSoftwareBitmapAsync(
BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
var buffer = new Windows.Storage.Streams.Buffer(
(uint)(bitmap.PixelWidth * bitmap.PixelHeight * 4));
bitmap.CopyToBuffer(buffer);
using var reader = DataReader.FromBuffer(buffer);
byte b = reader.ReadByte(), g = reader.ReadByte(),
r = reader.ReadByte(), a = reader.ReadByte();
return (r, g, b, a);
}
```
### Metadata Assertions
```csharp
// Before
Assert.AreEqual("Test", ((BitmapMetadata)image.Frames[0].Metadata).Comment);
// After
var props = await decoder.BitmapProperties.GetPropertiesAsync(
new[] { "System.Photo.DateTaken" });
Assert.IsTrue(props.ContainsKey("System.Photo.DateTaken"),
"Metadata should be preserved during transcode");
```
### AllowUnsafeBlocks for SoftwareBitmap Tests
If tests access pixel data via `IMemoryBufferByteAccess`, add:
```xml
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
```
---
## Settings JSON Backward Compatibility
- Settings are stored in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
- Schema must remain backward-compatible across upgrades
- Add new fields with defaults; never remove or rename existing fields
- Create custom enums matching WPF enum integer values for deserialization (e.g., `ImagingEnums.cs`)
- See: `src/settings-ui/Settings.UI.Library/`
## IPC Contract
If the module communicates with the runner or settings UI:
1. Update BOTH sides of the IPC contract
2. Test settings changes are received by the module
3. Test module state changes are reflected in settings UI
4. Reference: `doc/devdocs/core/settings/runner-ipc.md`
---
## Checklist for PowerToys Module Migration
### Project & Dependencies
- [ ] Update `.csproj`: `UseWPF``UseWinUI`, TFM → `net8.0-windows10.0.19041.0`
- [ ] Add `WindowsPackageType=None`, `SelfContained=true`, `WindowsAppSDKSelfContained=true`
- [ ] Add `DISABLE_XAML_GENERATED_MAIN` if using custom `Program.cs`
- [ ] Replace NuGet packages (WPF-UI → remove, add WindowsAppSDK, etc.)
- [ ] Update project references (GPOWrapperProjection → GPOWrapper + CsWinRT)
- [ ] Move `InternalsVisibleTo` from code to `.csproj`
- [ ] Extract CLI shared logic to Library project (avoid Exe→WinExe dependency)
### MVVM & Resources
- [ ] Replace custom `Observable`/`RelayCommand` with CommunityToolkit.Mvvm source generators
- [ ] Migrate `.resx``.resw` (`Properties/Resources.resx``Strings/en-us/Resources.resw`)
- [ ] Create `ResourceLoaderInstance` helper
- [ ] Wrap resource-dependent statics in `Lazy<T>` or null-coalescing properties
- [ ] Delete `Properties/Resources.Designer.cs`, `Observable.cs`, `RelayCommand.cs`
### XAML
- [ ] Replace `clr-namespace:``using:` in all xmlns declarations
- [ ] Remove WPF-UI (Lepo) xmlns and controls — use native WinUI 3
- [ ] Replace `{x:Static p:Resources.Key}``x:Uid` with `.resw` keys
- [ ] Replace `{DynamicResource}``{ThemeResource}`
- [ ] Replace `DataType="{x:Type ...}"``x:DataType="..."`
- [ ] Replace `<Style.Triggers>``VisualStateManager`
- [ ] Add `<XamlControlsResources/>` to `App.xaml` merged dictionaries
- [ ] Move `Window.Resources` to root container's `Resources`
- [ ] Run XamlStyler: `.\.pipelines\applyXamlStyling.ps1 -Main`
### Code-Behind & APIs
- [ ] Replace all `System.Windows.*` namespaces with `Microsoft.UI.Xaml.*`
- [ ] Replace `Dispatcher` with `DispatcherQueue`
- [ ] Store `DispatcherQueue` reference explicitly (no `Application.Current.Dispatcher`)
- [ ] Implement `SizeToContent()` via AppWindow if needed
- [ ] Update `ContentDialog` calls to set `XamlRoot`
- [ ] Update `FilePicker` calls with HWND initialization
- [ ] Migrate imaging code to `Windows.Graphics.Imaging` (async, `SoftwareBitmap`)
- [ ] Create `CodecHelper` for legacy GUID → WinRT codec ID mapping (if imaging)
- [ ] Create custom imaging enums for JSON backward compatibility (if imaging)
- [ ] Update all `IValueConverter` signatures (`CultureInfo``string`)
### Build & Installer
- [ ] Update WiX installer: remove satellite assembly refs from `Resources.wxs`
- [ ] Run `generateAllFileComponents.ps1`; handle phantom artifacts
- [ ] Update ESRP signing paths to `WinUI3Apps\\`
- [ ] Fix `$(SolutionDir)``$(MSBuildThisFileDirectory)` in build events
- [ ] Move MSIX packaging from PreBuild to PostBuild
- [ ] Fix RC file path escaping (double-backslash)
- [ ] Verify output dir is `WinUI3Apps/`
### Testing & Validation
- [ ] Update test project: async methods, `uint` assertions
- [ ] Handle ResourceLoader unavailability in tests (hardcode strings or skip)
- [ ] Build clean: `cd` to project folder, `tools/build/build.cmd`, exit code 0
- [ ] Run tests for affected module
- [ ] Verify settings JSON backward compatibility
- [ ] Test IPC contracts (runner ↔ settings UI)

View File

@@ -0,0 +1,314 @@
# 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
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
```
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)
```

View File

@@ -0,0 +1,365 @@
# XAML Migration Guide
Detailed reference for migrating XAML from WPF to WinUI 3, based on the ImageResizer migration.
## XML Namespace Declaration Changes
### Before (WPF)
```xml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyApp"
xmlns:m="clr-namespace:ImageResizer.Models"
xmlns:p="clr-namespace:ImageResizer.Properties"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
x:Class="MyApp.MainWindow">
```
### After (WinUI 3)
```xml
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp"
xmlns:m="using:ImageResizer.Models"
xmlns:converters="using:ImageResizer.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="MyApp.MainWindow">
```
### Key Changes
| WPF Syntax | WinUI 3 Syntax | Notes |
|------------|---------------|-------|
| `clr-namespace:Foo` | `using:Foo` | CLR namespace mapping |
| `clr-namespace:Foo;assembly=Bar` | `using:Foo` | Assembly qualification not needed |
| `xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"` | **Remove entirely** | WPF-UI namespace no longer needed |
| `xmlns:p="clr-namespace:...Properties"` | **Remove** | No more `.resx` string bindings |
| `sys:String` (from mscorlib) | `x:String` | XAML intrinsic types |
| `sys:Int32` | `x:Int32` | XAML intrinsic types |
| `sys:Boolean` | `x:Boolean` | XAML intrinsic types |
| `sys:Double` | `x:Double` | XAML intrinsic types |
## Unsupported Markup Extensions
| WPF Markup Extension | WinUI 3 Alternative |
|----------------------|---------------------|
| `{DynamicResource Key}` | `{ThemeResource Key}` (theme-reactive) or `{StaticResource Key}` |
| `{x:Static Type.Member}` | `{x:Bind}` to a static property, or code-behind |
| `{x:Type local:MyType}` | Not supported; use code-behind |
| `{x:Array}` | Not supported; create collections in code-behind |
| `{x:Code}` | Not supported |
### DynamicResource → ThemeResource
```xml
<!-- WPF -->
<TextBlock Foreground="{DynamicResource MyBrush}" />
<!-- WinUI 3 -->
<TextBlock Foreground="{ThemeResource MyBrush}" />
```
`ThemeResource` automatically updates when the app theme changes (Light/Dark/HighContrast). For truly dynamic non-theme resources, set values in code-behind or use data binding.
### x:Static Resource Strings → x:Uid
This is the most pervasive XAML change. WPF used `{x:Static}` to bind to strongly-typed `.resx` resource strings. WinUI 3 uses `x:Uid` with `.resw` files.
**WPF:**
```xml
<Button Content="{x:Static p:Resources.Cancel}" />
<TextBlock Text="{x:Static p:Resources.Input_Header}" />
```
**WinUI 3:**
```xml
<Button x:Uid="Cancel" />
<TextBlock x:Uid="Input_Header" />
```
In `Strings/en-us/Resources.resw`:
```xml
<data name="Cancel.Content" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="Input_Header.Text" xml:space="preserve">
<value>Select a size</value>
</data>
```
The `x:Uid` suffix (`.Content`, `.Text`, `.Header`, `.PlaceholderText`, etc.) matches the target property name.
### DataType with x:Type → Remove
**WPF:**
```xml
<DataTemplate DataType="{x:Type m:ResizeSize}">
```
**WinUI 3:**
```xml
<DataTemplate x:DataType="m:ResizeSize">
```
## WPF-UI (Lepo) Controls Removal
If the module uses the `WPF-UI` library, replace all Lepo controls with native WinUI 3 equivalents.
### Window
```xml
<!-- WPF (WPF-UI) -->
<ui:FluentWindow
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<ui:TitleBar Title="Image Resizer" />
...
</ui:FluentWindow>
<!-- WinUI 3 (native) -->
<Window>
<!-- Title bar managed via code-behind: this.ExtendsContentIntoTitleBar = true; -->
...
</Window>
```
### App.xaml Resources
```xml
<!-- WPF (WPF-UI) -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ui:ThemesDictionary Theme="Dark" />
<ui:ControlsDictionary />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<!-- WinUI 3 (native) -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### Common Control Replacements
```xml
<!-- WPF-UI NumberBox -->
<ui:NumberBox Value="{Binding Width}" />
<!-- WinUI 3 -->
<NumberBox Value="{x:Bind ViewModel.Width, Mode=TwoWay}" />
<!-- WPF-UI InfoBar -->
<ui:InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
<!-- WinUI 3 -->
<InfoBar Title="Warning" Message="..." IsOpen="True" Severity="Warning" />
<!-- WPF-UI ProgressRing -->
<ui:ProgressRing IsIndeterminate="True" />
<!-- WinUI 3 -->
<ProgressRing IsActive="True" />
<!-- WPF-UI SymbolIcon -->
<ui:SymbolIcon Symbol="Add" />
<!-- WinUI 3 -->
<SymbolIcon Symbol="Add" />
```
### Button Patterns
```xml
<!-- WPF -->
<Button IsDefault="True" Content="OK" />
<Button IsCancel="True" Content="Cancel" />
<!-- WinUI 3 (no IsDefault/IsCancel) -->
<Button Style="{StaticResource AccentButtonStyle}" Content="OK" />
<Button Content="Cancel" />
<!-- Handle Enter/Escape keys in code-behind if needed -->
```
## Style and Template Changes
### Triggers → VisualStateManager
WPF `Triggers`, `DataTriggers`, and `EventTriggers` are not supported.
**WPF:**
```xml
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue"/>
</Trigger>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</DataTrigger>
</Style.Triggers>
</Style>
```
**WinUI 3:**
```xml
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid x:Name="RootGrid" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="RootGrid.Background" Value="LightBlue"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
```
### No Binding in Setter.Value
```xml
<!-- WPF (works) -->
<Setter Property="Foreground" Value="{Binding TextColor}"/>
<!-- WinUI 3 (does NOT work — use StaticResource) -->
<Setter Property="Foreground" Value="{StaticResource TextColorBrush}"/>
```
### Visual State Name Changes
| WPF | WinUI 3 |
|-----|---------|
| `MouseOver` | `PointerOver` |
| `Disabled` | `Disabled` |
| `Pressed` | `Pressed` |
## Resource Dictionary Changes
### Window.Resources → Grid.Resources
WinUI 3 `Window` is NOT a `DependencyObject` — no `Window.Resources`, `DataContext`, or `VisualStateManager`.
```xml
<!-- WPF -->
<Window>
<Window.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
</Window.Resources>
<Grid>...</Grid>
</Window>
<!-- WinUI 3 -->
<Window>
<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="MyBrush" Color="Red"/>
</Grid.Resources>
...
</Grid>
</Window>
```
### Theme Dictionaries
```xml
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="MyBrush" Color="#FF000000"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="MyBrush" Color="#FFFFFFFF"/>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="MyBrush" Color="{ThemeResource SystemColorWindowTextColor}"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
```
## URI Scheme Changes
| WPF | WinUI 3 |
|-----|---------|
| `pack://application:,,,/MyAssembly;component/image.png` | `ms-appx:///Assets/image.png` |
| `pack://application:,,,/image.png` | `ms-appx:///image.png` |
| Relative path `../image.png` | `ms-appx:///image.png` |
Assets directory convention: `Resources/``Assets/<Module>/`
## Data Binding Changes
### {Binding} vs {x:Bind}
Both are available. Prefer `{x:Bind}` for compile-time safety and performance.
| Feature | `{Binding}` | `{x:Bind}` |
|---------|------------|------------|
| Default mode | `OneWay` | **`OneTime`** (explicit `Mode=OneWay` required!) |
| Context | `DataContext` | Code-behind class |
| Resolution | Runtime | Compile-time |
| Performance | Reflection-based | Compiled |
| Function binding | No | Yes |
### WPF-Specific Binding Features to Remove
```xml
<!-- These WPF-only features must be removed or rewritten -->
<TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
<!-- WinUI 3: UpdateSourceTrigger not needed; TextBox uses PropertyChanged by default -->
<TextBox Text="{x:Bind ViewModel.Value, Mode=TwoWay}" />
{Binding RelativeSource={RelativeSource Self}, ...}
<!-- WinUI 3: Use x:Bind which binds to the page itself, or use ElementName -->
<ItemsControl ItemsSource="{Binding}" />
<!-- WinUI 3: Must specify explicit path -->
<ItemsControl ItemsSource="{x:Bind ViewModel.Items}" />
```
## WPF-Only Window Properties to Remove
These properties exist on WPF `Window` but not WinUI 3:
```xml
<!-- Remove from XAML — handle in code-behind via AppWindow API -->
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
ExtendsContentIntoTitleBar="True" <!-- Set in code-behind -->
```
## XAML Control Property Changes
| WPF Property | WinUI 3 Property | Notes |
|-------------|-----------------|-------|
| `Focusable` | `IsTabStop` | Different name |
| `SnapsToDevicePixels` | Not available | WinUI handles pixel snapping internally |
| `UseLayoutRounding` | `UseLayoutRounding` | Same |
| `IsHitTestVisible` | `IsHitTestVisible` | Same |
| `TextBox.VerticalScrollBarVisibility` | `ScrollViewer.VerticalScrollBarVisibility` (attached) | Attached property |
## XAML Formatting (XamlStyler)
After migration, run XamlStyler to normalize formatting:
- Alphabetize xmlns declarations and element attributes
- Add UTF-8 BOM to all XAML files
- Normalize comment spacing: `<!-- text -->``<!-- text -->`
PowerToys command: `.\.pipelines\applyXamlStyling.ps1 -Main`