# PowerToys-Specific Migration Patterns Patterns and conventions specific to the PowerToys codebase, based on the ImageResizer migration. ## Project Structure ### Before (WPF Module) ``` src/modules// ├── UI/ │ ├── 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/ ├── CLI/ │ └── CLI.csproj # OutputType=Exe └── tests/ ``` ### After (WinUI 3 Module) ``` src/modules// ├── UI/ │ ├── 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/ │ └── / │ └── .ico # Moved from Resources/ ├── Common/ # NEW: shared library for CLI │ └── Common.csproj # OutputType=Library ├── CLI/ │ └── 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` `` | | `Helpers/Observable.cs` | Replaced by `CommunityToolkit.Mvvm.ObservableObject` | | `Helpers/RelayCommand.cs` | Replaced by `CommunityToolkit.Mvvm.Input` | | `Resources/*.ico` / `Resources/*.png` | Moved to `Assets//` | | 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 _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> _tokens = new(() => new Dictionary { ["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"), ["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"), }); // Usage: _tokens.Value.TryGetValue(...) ``` ### XAML: x:Static → x:Uid ```xml