Uno winui-port
Port WinUI C++ code to Uno Platform C#. Supports full control porting and snippet/incremental porting.
git clone https://github.com/unoplatform/uno
T=$(mktemp -d) && git clone --depth=1 https://github.com/unoplatform/uno "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/winui-port" ~/.claude/skills/unoplatform-uno-winui-port && rm -rf "$T"
.claude/skills/winui-port/SKILL.mdUser Input
$ARGUMENTS
You MUST consider the user input before proceeding (if not empty).
Overview
You are executing the WinUI Porting Skill. You produce lossless, structure-preserving, human-fixable drafts of Uno Platform C# code from WinUI C++ headers, implementations, IDL files, and related tests.
Your output must never lose information, never delete logic, and must follow the partial-file layout, event-revoker patterns, and Uno constraints defined in this document.
The user's input after
/winui-port is a free-form description of what to port. It may be:
- A full control name: "Port the Expander control from /path/to/winui/dev/Expander/"
- A snippet/method: "Port the OnApplyTemplate method from /path/to/InfoBar.cpp into the existing InfoBar port"
- A task description: "Port the remaining unported methods in TitleBar"
- File paths with instructions: "Port /path/to/SplitButton.cpp, /path/to/SplitButton.h, /path/to/SplitButton.idl"
Parse the intent and determine whether this is a full control port or a snippet/incremental port.
Execution Workflow
Phase 0: Analyze Intent and Gather Sources
-
Parse the user input to determine:
- Which C++ source files to read (
,.cpp
,.h
,_Partial.cpp
)_Partial.h - Whether an
(MIDL3) file exists alongside the source.idl - Whether this is a full control port or a snippet/incremental port
- The control name and namespace
- Which C++ source files to read (
-
Read all source files provided or referenced by the user. For a full control port, look for the complete set:
— main implementationControlName.cpp
— header with fields, constants, inline methodsControlName.h
/ControlName_Partial.cpp
— split implementations (if they exist)ControlName_Partial.h
— property definitionsControlName.properties.cpp
— MIDL3 API surface definitionControlName.idl- Associated types:
,ControlNameTemplateSettings.h/.cpp
,ControlNameAutomationPeer.h/.cpp
(auto-detect from the same directory)ControlNameEventArgs.h/.cpp - Test files: Look for
files in nearby test directories*ControlName*Test*
-
Determine the MUX reference tag/commit:
- Run
wheregit -C <winui-source-dir> rev-parse --short HEAD
is the root of the WinUI git repository (the directory containing the<winui-source-dir>
folder). Use this single commit hash for all.git
headers in the ported files.// MUX Reference - Run this command before writing any files so the correct hash is known up front.
- If the directory is not a git repository or the command fails, fall back to any tag embedded in the path (e.g.,
), or leave the value asrelease/1.6-stable
with aunknown
note.TODO Uno:
- Run
-
Read the IDL file (if found) to determine the complete public API surface including:
- All public properties and their types/defaults
- All public events
- All public methods
- Base class and implemented interfaces
- Enum types defined for the control
-
Fetch documentation from Microsoft Learn for the control being ported:
- Search for
to find the API referencesite:learn.microsoft.com "WinUI" "ControlName" - Extract
descriptions for public properties, events, and methods<summary> - These will be used for XML doc comments on public members in
.Properties.cs
- Search for
-
Check for existing ported code in the Uno repo:
- Search
for existing files matching the control namesrc/Uno.UI/ - If files exist, this informs whether to merge or create new files
- Check
for existing NotImplemented stubssrc/Uno.UI/Generated/
- Search
Phase 1: Determine Output Structure
For a full control port, plan these files:
| C++ Source | C# Output | Content |
|---|---|---|
| | Implementation (constructors, methods, overrides) |
| | Fields, constants, revokers, inline methods |
| | Split implementation (if exists) |
| | Split header fields (if exists) |
| | Public DependencyProperties, events |
| | Main class declaration (public partial class) |
| etc. | TemplateSettings (if exists) |
| etc. | AutomationPeer (if exists) |
| | EventArgs classes (if exist) |
For a snippet/incremental port, determine which existing files to update:
- If methods are being added to an existing control, merge into the appropriate existing
or.mux.cs
file.h.mux.cs - If new fields/constants are needed, add them to the
file.h.mux.cs - If new properties are being added, update
.Properties.cs
Output directory: Auto-detect based on the control's namespace:
→Microsoft.UI.Xaml.Controls.ControlNamesrc/Uno.UI/Microsoft/UI/Xaml/Controls/ControlName/
→Microsoft.UI.Xaml.Controls.Primitives.ControlNamesrc/Uno.UI/Microsoft/UI/Xaml/Controls/Primitives/ControlName/- Create the directory if it does not exist
Phase 2: Port the Code
Apply ALL of the following porting rules when converting C++ to C#.
2.1. General Rules
- Never remove or simplify code. Anything you cannot convert must be preserved as a comment with a clear
explanation.TODO Uno: - Preserve all
code comments exactly. Every comment in the C++ source — explanatory notes, rationale, section dividers,//
labels (converted to#pragma region
), TODOs, and inline remarks — must appear in the C# output at the same relative position. Do not paraphrase, summarize, or omit comments. The only acceptable changes are adapting C++ syntax references inside a comment to their C# equivalents (e.g.,// #pragma region
→winrt::hstring
).string - Maintain method order and structure exactly as in the original C++ files.
- Preserve all behavior and intent, even if the resulting C# does not compile yet.
- Any Uno-specific code must be wrapped in
/#if HAS_UNO
.#endif - Any original code that cannot be represented in Uno must be wrapped in
/#if !HAS_UNO
with a#endif
comment explaining what is missing.TODO Uno: - The conversion is a draft, not a verified build. It may contain unresolved symbols or unimplemented APIs — leave them visible.
- Line number reference comments (e.g.,
) must only be added when porting small snippets or individual methods into an existing file. When porting a whole file or large section, do not add per-method line number comments — they clutter the code and become stale. The MUX reference header comment at the top of the file is sufficient for traceability.// Layout.cpp, line 237
2.2. File Headers
Every generated file MUST start with the license header followed by the MUX reference:
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. // MUX Reference <SourceFile.cpp>, tag winui3/release/1.6-stable
Or with commit hash:
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See LICENSE in the project root for license information. // MUX Reference <SourceFile.cpp>, commit 9d6fb15c0
2.3. File Layout Rules
MANDATORY: Every ported C++ source pair (
ControlName.h + ControlName.cpp) MUST be broken down into separate partial-class files. A single monolithic .cs file containing both field declarations and method implementations is never acceptable — even for small or simple types. The required split is:
| File | Content |
|---|---|
| Declaration only — with the MUX reference header, XML doc , and nothing else |
| From the C++ header — private fields, constants, inline method bodies. Omit this file only if the header has zero fields and zero inline method bodies |
| From the C++ implementation — all method bodies, in the same order as the file |
| From — fields and inline methods from the partial header. Only when exists. Must be a separate file — do not merge into |
| From — method bodies from the partial implementation. Only when exists. Must be a separate file — do not merge into |
| Uno-specific additions — WeakEventHelper hooks, platform workarounds, members with . Omit if none exist |
| Public DependencyProperties and events — only when the type defines DPs. Omit otherwise |
Each C++ source file maps to exactly one C# output file.
_Partial.cpp and _Partial.h are separate translation units in WinUI and must remain separate files in the port — never combine them with the main .mux.cs / .h.mux.cs.
When an existing monolithic
.cs file is encountered during porting, it must be refactored into the above split as part of the port.
Main class file (
):ControlName.cs
- Contains only the public class declaration:
public partial class ControlName : BaseType { } - No fields, no methods, no properties.
Implementation file (
/ ControlName.mux.cs
):ControlName.partial.mux.cs
- Contains the converted
implementation..cpp - Includes constructors, method bodies, overrides, event hookup logic.
- Maintain the exact method order from the C++ source.
- Uses
with no access modifiers on the class declaration.partial class
Header file (
/ ControlName.h.mux.cs
):ControlName.partial.h.mux.cs
- Field declarations (references to template child elements, cached control references, revokers, state variables)
- Constants (template part names, visual state names)
- All methods that have an inline body in the C++ header (e.g.,
), regardless of their access modifier or whether they are public API — the deciding factor is where the body lives in C++, not where it logically "belongs"virtual Foo Bar() { return nullptr; } - Dependency-property-related arrays/metadata
- SerialDisposable revoker fields
- DispatcherHelper instances
- Uses
with no access modifiers on the class declaration.partial class
Properties file (
):ControlName.Properties.cs
- Contains public properties and public events that form the API surface.
- Uses private fields defined in
..h.mux.cs - Only the API surface goes here — no implementation logic.
- All public members MUST have XML documentation comments (sourced from Microsoft Learn).
- Uses
with no access modifiers on the class declaration.partial class
2.3.1. Method Style
For single-line method bodies, prefer expression-body syntax (
=>):
// Prefer this: internal int LayoutAnchorIndexDbg() => m_layoutAnchorInfoDbg.Index; internal IndexBasedLayoutOrientation GetForcedIndexBasedLayoutOrientationDbg() => m_forcedIndexBasedLayoutOrientationDbg; // Over this: internal int LayoutAnchorIndexDbg() { return m_layoutAnchorInfoDbg.Index; }
This applies to both methods and properties. Use block-body syntax only when the body has more than one statement.
2.4. C++ to C# Syntax Conversions
Apply these conversions systematically:
| C++ Pattern | C# Equivalent |
|---|---|
| |
(property getter) | |
| |
| |
| |
| (direct access) |
| (with ) |
event subscription | pattern (see section 2.5) |
C++ destructor () | No finalizer — comment out with (see section 2.6) |
| |
| |
| |
| or |
| |
or | |
2.5. Event Handling and Revokers
C++ revokers (
auto_revoke, revoker tokens, vector-changed tokens, per-item maps) must be converted to SerialDisposable patterns.
When converting, check for common leak scenarios:
- Event handlers on long-lived sources (
, singletons, static events)this - Per-item or per-token subscriptions stored in collections
- Circular references between publishers and subscribers
- Platform-specific lifecycle issues (iOS views/controllers that may outlive expected scope)
Add
// TODO Uno: Investigate potential leak: <short-reason> when a potential leak is suspected.
Basic revoker pattern:
private readonly SerialDisposable _myEventRevoker = new SerialDisposable(); void InitializeSomething() { source.Event += Handler; _myEventRevoker.Disposable = Disposable.Create(() => { source.Event -= Handler; }); }
CompositeDisposable for multiple events in OnApplyTemplate:
private readonly SerialDisposable _eventSubscriptions = new SerialDisposable(); protected override void OnApplyTemplate() { _eventSubscriptions.Disposable = null; var registrations = new CompositeDisposable(); _eventSubscriptions.Disposable = registrations; if (GetTemplateChild<Border>(c_contentClip) is Border contentClip) { contentClip.SizeChanged += OnContentClipSizeChanged; registrations.Add(() => contentClip.SizeChanged -= OnContentClipSizeChanged); } }
Loaded/Unloaded lifecycle pattern (for controls needing cleanup):
#if HAS_UNO // Uno specific: we might leak on iOS Loaded += Control_Loaded; Unloaded += Control_Unloaded; #endif private void Control_Loaded(object sender, RoutedEventArgs e) { if (_eventSubscriptions.Disposable is null) { OnApplyTemplate(); } } private void Control_Unloaded(object sender, RoutedEventArgs e) => _eventSubscriptions.Disposable = null;
2.6. Destructors and Cleanup
Uno Platform does not use finalizers for event cleanup. When C++ contains a destructor:
- Do not generate a C# finalizer.
- Emit the destructor logic as commented-out code inside
:#if HAS_UNO
#if HAS_UNO // TODO Uno: Original C++ destructor cleanup. Uno does not support cleanup via finalizers. // Move this logic into Loaded/Unloaded event handlers or other lifecycle methods to avoid leaks. // Original destructor logic (not executed): // <... commented original cleanup ...> #endif
All cleanup steps must remain in comments exactly as they appeared in C++.
2.7. Common Helpers
| C++ Pattern | C# Helper |
|---|---|
| |
| (only for WinUI-only controls not in UWP) |
| Localized string resources | |
| |
| |
| with |
2.8. Conditional Compilation
Uno-specific code (Uno helpers, Uno macros, Uno-specific constructs):
#if HAS_UNO // Uno-specific implementation #endif
Composition/Visual-layer APIs that are not fully available in Uno:
#if !HAS_UNO // WinUI implementation using Composition APIs var visual = ElementCompositionPreview.GetElementVisual(element); visual.Clip = visual.Compositor.CreateInsetClip(); #else // TODO Uno specific: The Composition clipping APIs are currently unsupported. // Using UIElement.Clip or SizeChanged workaround instead. element.SizeChanged += OnElementSizeChanged; registrations.Add(() => element.SizeChanged -= OnElementSizeChanged); #endif
WinUI import differences:
#if HAS_UNO_WINUI using ITextSelection = Microsoft.UI.Text.ITextSelection; #else using ITextSelection = Windows.UI.Text.ITextSelection; #endif
2.9. Dependency Properties (in .Properties.cs
)
.Properties.cs/// <summary> /// Gets or sets a value indicating whether the control is expanded. /// </summary> public bool IsExpanded { get => (bool)GetValue(IsExpandedProperty); set => SetValue(IsExpandedProperty, value); } public static DependencyProperty IsExpandedProperty { get; } = DependencyProperty.Register( nameof(IsExpanded), typeof(bool), typeof(ControlName), new FrameworkPropertyMetadata( false, (s, e) => (s as ControlName)?.OnIsExpandedPropertyChanged(e)));
2.10. TODO Comment Conventions
Use consistent TODO formats:
| Scenario | Format |
|---|---|
| Uno workaround | |
| Missing API | |
| Unimplemented section | Wrap in with comment |
| Future work | |
| Not ported yet | |
| Potential leak | |
| Stub method | |
2.11. Member Visibility
Default to the most restrictive visibility that still compiles. Only widen when there is explicit evidence from one of these authoritative sources:
- The IDL file declares the member as public/protected
- Microsoft Learn documentation lists it as part of the public API
- The member is already declared in Uno's Generated stub as public/protected
- The member is needed by another class in the same assembly (use
)internal - The member must be overridable by subclasses outside the assembly (use
)protected
When none of the above apply, use
private. When the member must be accessible to subclasses only within the assembly, use private protected. Specific rules:
- No new
members unless strictly required by the API surface.public - Prefer
for all implementation details, helper methods, and fields.private - Use
for members that subclasses need to call or override, but that are not part of the published API — including debug/test-hook members guarded byprivate protected
in C++ or documented as "for testing purposes only".#ifdef DBG - Use
only when accessed by other Uno infrastructure types within the same assembly.internal - Use
only when the IDL, Microsoft Learn, or Generated stubs confirm the member is part of the public overridable API surface.protected - Never widen visibility based on convenience, grouping preference, or to simplify testing.
2.12. XML Documentation
All ported public or protected members must include XML documentation comments sourced from the Microsoft Learn API reference fetched in Phase 0. This includes:
properties, methods, and eventspublic
andprotected
virtual/override methods and properties (since subclasses will see them)protected internal
Use
<summary>, <param>, <returns>, and <value> tags as appropriate, copying the exact wording from Microsoft Learn where available:
/// <summary> /// Gets the orientation, if any, in which items are laid out based on their /// index in the source collection. /// </summary> /// <value> /// A value of the enumeration that indicates the orientation. The default is /// <see cref="IndexBasedLayoutOrientation.None"/>. /// </value> public IndexBasedLayoutOrientation IndexBasedLayoutOrientation => ...; /// <summary> /// Sets the value of the <see cref="IndexBasedLayoutOrientation"/> property. /// </summary> /// <param name="orientation"> /// A value of the enumeration that indicates the orientation, if any, in which /// items are laid out based on their index in the source collection. /// </param> protected void SetIndexBasedLayoutOrientation(IndexBasedLayoutOrientation orientation) => ...;
If Microsoft Learn has no documentation for a member (e.g., it is new or internal-only), write a brief
<summary> that describes what the method does based on the C++ source. Do not leave public/protected members undocumented.
Phase 3: Handle Associated Types
Auto-detect and port these associated types from the same source directory:
- TemplateSettings (
): Port as a separate class with its ownControlNameTemplateSettings
file..cs - AutomationPeer (
): Port as a separate class following the same file layout patterns.ControlNameAutomationPeer - EventArgs (
,ControlNameEventArgs
, etc.): Port each as its ownControlNameChangedEventArgs
file..cs - Enums: Port any enum types defined in the IDL that are used by the control.
Phase 4: Handle Resources
-
ResourceAccessor constants: Find the
class (search forResourceAccessor
inResourceAccessor.cs
) and add newsrc/Uno.UI/
constants for any localized strings used by the ported control.SR_ -
Strings folder: If the WinUI source directory contains a
folder withStrings/
localized string files, copy them to the appropriate location in the Uno project (typically alongside the control's output directory or in the shared resources location). Maintain the folder structure (e.g.,.resw
).Strings/en-US/Resources.resw
Phase 5: Handle XAML Styles
- Check if the control has a XAML template/style in the WinUI source (typically in a
or similar file in the control's source directory).*_themeresources.xaml - Check if an equivalent style already exists in the Uno repo under
(search for the control name insrc/Uno.UI.FluentTheme/
files)..xaml - If the style does not exist or differs significantly:
- For new controls: Create the style in
(search the repo for the exact directory structure used by similar controls).Uno.UI.FluentTheme.v2 - For existing controls with updates: Update the existing style file.
- For new controls: Create the style in
Phase 6: Update Generated Files
- Find the Generated stub files for the ported types in
(andsrc/Uno.UI/Generated/
if applicable).src/Uno.UWP/Generated/ - For each newly implemented member:
- Remove the member from the
attribute's platform list, or remove the attribute entirely if all platforms are now implemented.[NotImplemented] - Adjust
directives to reflect which platforms have the implementation.#if
- Remove the member from the
- NEVER put any implementation code into Generated files — only modify attributes and
directives.#if
Phase 7: Port Tests
- Look for test files associated with the control in the WinUI source:
- Unit tests (typically in a
directory near the control source)*Tests* - UI/interaction tests
- Unit tests (typically in a
- Convert C++ tests to Uno RuntimeTests format:
- Place tests in
in an appropriate subdirectorysrc/Uno.UI.RuntimeTests/Tests/ - Use the Uno RuntimeTest patterns:
[TestMethod] [RunsOnUIThread] public async Task When_ControlName_Does_Something() { var control = new ControlName(); WindowHelper.WindowContent = control; await WindowHelper.WaitForLoaded(control); await WindowHelper.WaitForIdle(); // Assert behavior... } - Convert C++ test assertions to MSTest assertions (
,Assert.AreEqual
, etc.)Assert.IsTrue - Preserve the intent and coverage of each original test
- Place tests in
Phase 8: Build Check (Optional)
After all files are written, ask the user:
"Would you like me to run a build to check for compilation errors?"
If the user agrees:
- Run
on the appropriate solution filterdotnet build - Report any errors in the summary
- Suggest fixes for common issues
Phase 9: Summary
Produce a detailed summary containing:
- Files created/modified: Full list with paths
- Porting scope: Full control vs. snippet, what was included
- Associated types ported: TemplateSettings, AutomationPeer, EventArgs, Enums
- Resources added: New SR_ constants, copied Strings folders
- XAML styles: Created or updated style files
- Generated files updated: Which NotImplemented attributes were modified
- Tests ported: List of test methods converted
- TODO items: All
items generated, categorized by:TODO Uno:- Missing APIs / unresolved symbols
- Potential leaks
- Unported sections
- Composition API workarounds
- Potential issues: Anything that may need manual attention
- Next steps: Recommended actions (build, test, review specific TODOs)
Porting Rules Quick Reference
This section is a condensed reference of all conversion patterns. Use it as a checklist while porting.
Namespace Mappings
| C++ | C# |
|---|---|
| |
| |
| |
| |
Type Mappings
| C++ Type | C# Type |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| (direct field) |
| |
| (direct reference) |
| C# event declaration |
Keyword Mappings
| C++ | C# |
|---|---|
| |
/ | / |
| or |
| |
| |
/ | |
| |
| |
| or |
Method Pattern Mappings
| C++ Pattern | C# Pattern |
|---|---|
(property getter call) | (property access) |
(property setter call) | |
| |
| |
| |
| |
| |
| |
| |
| |