Winforms To Avalonia
tautcony/ChapterTool
Orchestrate a phased WinForms-to-Avalonia migration: behavior contracts, UI-independent application logic, platform adapters, MVVM shell, parity/cutover, evolution, and hardening.
Comprehensive Avalonia 12 migration tool for AtomUI. An agent skill from AtomUI/AtomUI.
The automated check flagged lines worth reading first. See the safety section below.
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12 --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .claude/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .claude/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12Type this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12 --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .agents/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .agents/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12 --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .cursor/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .cursor/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/AtomUI/AtomUI.git --path .agents/skills/migrate-to-avalonia12--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12 --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .gemini/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .gemini/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12Installs for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .github/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .github/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install AtomUI/AtomUI migrate-to-avalonia12 --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/AtomUI/AtomUI.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/migrate-to-avalonia12 .opencode/skills/migrate-to-avalonia12 && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "migrate-to-avalonia12" agent skill from https://github.com/AtomUI/AtomUI/tree/release%2F6.0/.agents/skills/migrate-to-avalonia12 into .opencode/skills/migrate-to-avalonia12/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrate-to-avalonia12", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
migrate-to-avalonia12Comprehensive Avalonia 12 migration tool for AtomUI. An agent skill from AtomUI/AtomUI.
Migrate To Avalonia12 is an agent skill from AtomUI/AtomUI. Comprehensive Avalonia 12 migration tool for AtomUI. Detects and fixes all breaking changes from Avalonia 11 to 12, covering 50+ categories including focus events, TopLevel API, clipboard changes, binding system updates, PlacementMode rename, [PrivateApi] public interface handling (IInputRoot still usable), IPopupHost/Gestures internalization, window decoration redesign, dispatcher changes, obsolete member removals, renamed members, internal API extraction strategy, ReflectionExtensions for internal members…
Its SKILL.md is about 24k tokens, which your agent loads only when the skill is triggered. The skill folder holds 8 other files, including reference files (for example `references/atomui-migration-guide.md`, `references/avalonia12-breaking-changes.md` and `references/code-level-analysis.md`).
It sits in Development. It works with Avalonia and .NET. The repository describes itself as: An enhancement and extension library for Avalonia, bringing the Ant Design design language, modern controls, theming, native integrations, and cross-platform UI capabilities to… The licence is LGPL-3.0.
12 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit e82b36c. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
dotnetrgFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
atomui.netgithub.comschemas.microsoft.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Migrate To Avalonia12 loads about 24k tokens when it runs, and up to ~43k if it reads all its reference files. Until then it costs about 164 tokens; SKILL.md has 7,690 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found patterns that need a careful read before installing.
- Fixing without user consentAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from AtomUI/AtomUI at commit e82b36c, republished under its LGPL-3.0 licence (© AtomUI). 7,690 words, ~23,807 tokens.
.claude/skills/migrate-to-avalonia12/SKILL.md (or your agent's skills folder). This skill also uses 7 other files; get the full folder from GitHub.1. Detect all breaking changes — Scan code for Avalonia 11 API usage incompatible with Avalonia 12, covering 50+ categories of changes.
2. Provide comprehensive guidance — Explain what changed, why, and how to fix it with clear examples.
3. Support multi-platform migration — Handle desktop, Android, iOS, browser, and headless platform changes.
4. Prioritize by impact — Categorize issues by severity and auto-fixability.
5. Ensure AOT compatibility — Identify and fix reflection-based API access with proper [DynamicDependency] patterns.
6. Verify compilation — After applying code changes, always run dotnet build to ensure the migrated code compiles successfully. Migration is only complete when the build succeeds.
CRITICAL: Avalonia 12 migration is ONLY about API compatibility, NOT about changing functionality.
Avalonia 12 is a major version with significant breaking changes across binding system, focus handling, clipboard API, window decorations, TopLevel architecture, Popup positioning, extension methods, dispatcher model, obsolete member removals, renamed APIs, and platform support. This skill automates detection and fixing of the most common issues while providing guidance for complex migrations and AOT-safe reflection patterns.
The ONLY goal of migration is to make the code compile and run on Avalonia 12 with IDENTICAL behavior to the original. Any change in control behavior, logic, or functionality is a migration error, not an improvement.
Use this skill when the user:
ABSOLUTE RULE: Migration to Avalonia 12 MUST NOT change any control behavior, logic, or functionality. The ONLY purpose of migration is to replace Avalonia 11 APIs with Avalonia 12 equivalents while preserving 100% identical behavior.
Examples of FORBIDDEN changes during migration:
SetCurrentValue(IsSelectedProperty, true) in HandleSubMenuOpenChanged)if (menuItem.IsTopLevel) checks that didn't exist before)PointerMoved, KeyDown) that didn't exist in the originalONLY allowed changes:
IRenderRoot → TopLevel, GotFocusEventArgs → FocusChangedEventArgs)using Avalonia.Rendering if no longer needed)Verification before committing:
Real-world example of migration error:
// ❌ WRONG - Added logic that didn't exist in release/5.0
if (value)
{
foreach (var item in ItemsView.OfType<NavMenuItem>())
{
item.TryUpdateCanExecute();
}
SetCurrentValue(IsSelectedProperty, true); // ← This is NEW logic, NOT an API change!
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
}
// ✅ CORRECT - Only API changes, logic unchanged
if (value)
{
foreach (var item in ItemsView.OfType<NavMenuItem>())
{
item.TryUpdateCanExecute();
}
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
}Check all 50+ categories of breaking changes, not just the most common ones. Pay special attention to:
For any migration scoped to a file, folder, module, project, or branch port, you MUST treat the requested scope as a closed set and verify it end-to-end:
Why: Avalonia 12 migration often leaves behind APIs that still compile but violate the migration goal. Build success alone is not a sufficient completion signal.
Minimum residual scan set for Avalonia 12 migrations:
Dispatcher\\.UIThreadPlacementMode\\s*=MotionAwareOpen|MotionAwareCloseGestures\\.KeyboardNavigationHandlerBindingPluginsDataFormats\\.\\bWatermark\\b|UseFloatingWatermark\\bSystemDecorations\\b|ExtendClientAreaChromeHintsAdd more patterns as needed based on the target module. The important rule is: scan the full requested scope, not only edited files.
Consider target platforms (Desktop, Android, iOS, Browser, Headless) when suggesting fixes.
Explain the rationale behind each breaking change from Avalonia's perspective.
For removed APIs, always provide the recommended replacement.
Analyze and report without modifying files unless explicitly requested.
Scan for custom extension methods that use removed or internalized APIs like:
GetVisualRoot() using GetPresentationSource()GetRootElement() on IInputRoot (note: IInputRoot itself is still public and usable)Scan for renamed APIs that will still compile but may cause confusion:
Popup.PlacementMode → Popup.Placement (property renamed, enum still exists)TextBox.Watermark → TextBox.PlaceholderTextTextBox.UseFloatingWatermark → TextBox.UseFloatingPlaceholderWindow.SystemDecorations → Window.WindowDecorationsRenderOptions.TextRenderingMode → TextOptions.TextRenderingModeTextBlock.LetterSpacing → TextElement.LetterSpacingColor.ToUint32() → Color.ToUInt32() (case change)Screen.PixelDensity → Screen.ScalingScreen.Primary → Screen.IsPrimaryBindingPriority.TemplatedParent → BindingPriority.TemplateCubicBezierEasing → SplineEasingCustomAnimatorBase → InterpolatingAnimator<T>ContextMenu.PlacementMode → ContextMenu.PlacementPseudolassesExtensions → PseudoClassesExtensions (typo fix)[PrivateApi] is a documentation attribute — it does NOT change the accessibility of the type. A public interface annotated with [PrivateApi] is still fully compilable and usable. NEVER replace such interfaces with alternatives like TopLevel.
[PrivateApi] public interfaces (USE DIRECTLY): IInputRoot — still public, compiles fine, use as-isinternal interfaces (NEED reflection or extraction): IRenderRoot, ILayoutRoot — cannot be referenced from external assembliesWhen you encounter [PrivateApi] on a public type, check the actual C# access modifier. If it's public, use it directly. Only use reflection/extraction for types that are genuinely internal or private.
AtomUI already has a comprehensive set of ReflectionExtensions that wrap internal/private Avalonia members. Before writing new reflection code during migration, check the AtomUI ReflectionExtensions Catalog section below. If an extension already exists for the member you need, use it directly. Only create new ReflectionExtensions when no existing one covers the target member.
When accessing private/internal Avalonia APIs:
[DynamicDependency] attributes to mark members for AOT preservationLazy<T> to cache reflection infoGetXxxInfoOrThrow() for safe reflectionAtomUI uses a layered architecture for multi-platform support:
AtomUI.Controls — Platform-agnostic base controls (shared across all platforms)AtomUI.Desktop.Controls — Desktop-specific control implementationsAtomUI.Mobile.Controls (planned) — Mobile-specific control implementationsDesktop and Mobile controls typically inherit from abstract base classes in AtomUI.Controls. For example:
AtomUI.Desktop.Controls.ScrollBar → AtomUI.Controls.Commons.AbstractScrollBarAtomUI.Desktop.Controls.ScrollViewer → AtomUI.Controls.Commons.AbstractScrollViewerWhen migrating a control in AtomUI.Desktop.Controls (or AtomUI.Mobile.Controls):
AtomUI.Controls, scan it for breaking changes tooWhy: Breaking changes in the base class affect all platform-specific implementations. Missing base class issues leads to runtime bugs (e.g., RawInputEventArgs.Root comparison failures) that are hard to trace back to the migration.
When you need to check whether an API exists, what properties/methods a type exposes, or how a type is defined, look up the source code under .referenceprojects/ first. These are local checkouts of the exact versions used by the project. Do NOT attempt to decompile NuGet assemblies, parse strings output, or guess API shapes. Source is authoritative and always available.
Available reference repositories:
.referenceprojects/Avalonia/src — Avalonia 12 core (Avalonia.Base / Avalonia.Controls / Avalonia.Themes.Fluent / Avalonia.Skia / platform backends). Use for all Avalonia 11→12 breaking-change verification, accessibility checks (public / [PrivateApi] / internal), template slot names, default property values..referenceprojects/Avalonia.Controls.DataGrid — Avalonia official DataGrid. Use when AtomUI's DataGrid derives from or aligns with upstream behavior..referenceprojects/Svg.Skia/src/Svg.Controls.Avalonia — Svg.Controls.Avalonia package source (the Avalonia.Svg.Svg control, AvaloniaPicture, SvgSource, etc.). Use when touching atom:ImagePreviewer, atom:Empty, atom:Result or anywhere SVG is rendered..referenceprojects/avalonia-docs — Avalonia official docs (api/, api_versioned_docs/). Use for conceptual/migration prose, API usage examples, and cross-version comparisons before confirming details in the code tree above.Examples:
TextOptions has a TextRenderingModeProperty: rg in .referenceprojects/Avalonia/src/Avalonia.Base and Avalonia.ControlsIInputManager is still public: read the interface definition in .referenceprojects/Avalonia/srcAvalonia.Svg.Svg exposes in 12.0.0.5: .referenceprojects/Svg.Skia/src/Svg.Controls.Avalonia/Svg.cs (e.g. Model is SKPicture?, there is no GetSKPicture()).referenceprojects/Avalonia.Controls.DataGridRule: Before making any claim like "API X was removed" / "method Y is internal" / "property Z exists", grep one of these four trees. If you haven't looked at the source, don't speculate — reflect reading NuGet metadata is lossy and has already caused incorrect conclusions (e.g. claiming Avalonia.Svg.Svg didn't exist when it very much does).
When migrating a control module, the following files do NOT need breaking-change scanning or dependency analysis — copy them directly from release/5.0 to the target branch:
*Token.cs — Control design token definitions (e.g., AlertToken.cs, AdornerLayerToken.cs). Only apply the ScopeProvider field addition to match main's pattern.*LangResource*.cs / *Lang*.resx — Control language pack / localization resource files.These files contain only data declarations (token values, string resources) with no Avalonia API usage that could be affected by breaking changes. Scanning them wastes time.
CRITICAL PRINCIPLE: Migration to Avalonia 12 or code optimization MUST NOT change the control's behavior, functionality, or user-facing features. This applies to both controls and their ShowCases.
What MUST be preserved:
What you CAN change for Avalonia 12:
IDataObject → IAsyncDataTransfer)IRenderRoot, ILayoutRoot)What you CANNOT change:
What MUST be preserved:
ShowCasePanel.Styles, keep it exactly as-is.IList<T>? vs IList?, bool vs bool?). Type changes can break binding or change null-handling behavior.CommandParameter="{Binding ElementName=...}", keep it. Don't assume ReactiveUI bindings can replace all patterns.CRITICAL: Do NOT redefine existing types
Before defining any class, interface, or enum in a ShowCase ViewModel:
src/AtomUI.Controls first — Most control-related types (e.g., CheckBoxOption, RadioOption, SelectOption) are already defined in the control's namespacesrc/AtomUI.Desktop.Controls — Platform-specific types may be defined hereCheckBoxOption when AtomUI.Controls.CheckBoxOption exists causes type mismatches, binding failures, and runtime errorsExample of what NOT to do:
// WRONG - Redefining CheckBoxOption in ViewModel file
public class CheckBoxOption
{
public string? Content { get; set; }
public bool IsEnabled { get; set; } = true;
}Correct approach:
// RIGHT - Use the official type from AtomUI.Controls
using AtomUI.Controls;
// CheckBoxOption is already defined in AtomUI.Controls namespace
// Just use it directly with IList<CheckBoxOption>Verification before committing:
find src -name "*.cs" -exec grep -l "class YourType" {} \;src/, use it. Do NOT redefine it.What you CAN change for Avalonia 12:
xmlns:vm="using:..." and x:DataType="vm:XxxViewModel" for compiled bindingsShowCaseViewModelBase to ReactiveObject, IRoutableViewModel (release/6.0 pattern)IActivatableViewModel and ViewModelActivator (release/6.0 doesn't use them)public static EntityKey ID unchanged (release/6.0 pattern) — do NOT change to public const string IDpublic string UrlPathSegment { get; } = ID; to public string? UrlPathSegment => ID.ToString(); (release/6.0 pattern)ReactiveCommand<Button, Unit> to ReactiveCommand<Unit, Unit> and remove sender parameters (Avalonia 12 optimization)using ReactiveUI.Avalonia and using System.Reactive.Disposables.Fluent if neededCRITICAL: Code-behind initialization must match release/5.0
ShowCase views in release/5.0 use ReactiveUserControl<TViewModel> with WhenActivated for initialization. When migrating:
release/5.0:controlgallery/.../Views/.../XxxShowCase.axaml.cs to see if it has WhenActivated logicWhenActivated, copy them exactlyReactiveUserControl<T> + WhenActivated with just UserControl, IViewFor<T> if the original has initialization logicWhenActivated to create and bind Marks to all 7 Slider controls. Without it, Marks won't display.Example of WRONG migration (missing initialization):
// WRONG - Simplified to basic IViewFor, lost all initialization logic
public partial class SliderShowCase : UserControl, IViewFor<SliderViewModel>
{
public SliderShowCase() { InitializeComponent(); }
object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as SliderViewModel; }
public SliderViewModel? ViewModel { get; set; }
}Correct migration (preserves initialization):
// RIGHT - Keeps ReactiveUserControl + WhenActivated with all initialization
public partial class SliderShowCase : ReactiveUserControl<SliderViewModel>
{
public SliderShowCase()
{
this.WhenActivated(disposables =>
{
if (DataContext is SliderViewModel viewModel)
{
// Copy ALL initialization from release/5.0
var marks = new List<SliderMark>();
marks.Add(new SliderMark("0°C", 0));
// ... rest of initialization
viewModel.SliderMarks = marks;
// Copy ALL bindings from release/5.0
this.OneWayBind(ViewModel, vm => vm.SliderMarks, v => v.Slider1.Marks)
.DisposeWith(disposables);
// ... rest of bindings
}
});
InitializeComponent();
}
}Verification checklist before committing:
WhenActivated logic before writing the migrationWhenActivated block, every binding, every subscriptionIList<T>? vs IList?, bool vs bool?, etc.Check all 50+ categories:
Core Framework (13 categories)
internal, data validation disabled by default)protected internal, IInputRoot still public with [PrivateApi], IRenderRoot/ILayoutRoot truly internal)Data & Clipboard (4 categories) 14. Clipboard API (IDataObject → IAsyncDataTransfer) 15. Drag-drop API (DoDragDrop → DoDragDropAsync) 16. DataFormats (DataFormats.* → DataFormat.*) 17. Windows BinaryFormatter removed (explicit serialization needed for clipboard)
Text & Rendering (5 categories)
18. Text formatting constructors (parameter order changed)
19. Access keys (now triggered by symbol, not virtual key; AccessKey is string?)
20. Font support (Type 1 fonts no longer supported)
21. Direct2D1 removed (use Skia instead)
22. Render target and platform surface interfaces reworked (CRITICAL for custom backends)
Platform-Specific (9 categories)
23. Android app initialization (AvaloniaMainActivity non-generic + AvaloniaAndroidApplication<TApp>)
24. Android lifetime (IActivityApplicationLifetime replaces ISingleViewApplicationLifetime)
25. Android CreateAppBuilder/CustomizeAppBuilder removed
26. iOS scene-based lifecycle (AvaloniaAppDelegate.Window stays null)
27. Browser Blazor package (removed, use Avalonia.Browser)
28. Tizen support (removed)
29. Diagnostics package (Avalonia.Diagnostics → AvaloniaUI.DiagnosticsSupport)
30. xUnit.net v3 (updated from v2)
31. NUnit v4 (updated from v3)
API Changes (12 categories)
32. Screen class (now abstract)
33. ResourcesChangedEventArgs (now readonly record struct)
34. Gesture events (Gestures class now internal, events moved to InputElement)
35. Window.WindowState (now direct property, not styled)
36. Data validation (enabled by default in custom controls)
37. IPopupHost now internal (was public)
38. IRenderer now [PrivateApi] (was public)
39. VisualLayerManager changes (AdornerLayer/OverlayLayer access changed)
40. FuncMultiValueConverter (new IReadOnlyList<TIn> constructor, IEnumerable kept for compat)
41. Popup changes (new properties: OverlayDismissEventPassThrough, ShouldUseOverlayLayer, etc.)
42. Popup.PlacementMode renamed to Popup.Placement (enum PlacementMode still exists)
Renamed & Removed Members (5 categories)
43. Renamed members (TextBox.Watermark→PlaceholderText, RenderOptions→TextOptions, etc.)
44. Comprehensive obsolete member removals (40+ items from Avalonia 11 now removed)
45. Extension methods & helper utilities using internalized APIs
46. ReflectionExtensions pattern for AOT (DynamicDependency, Lazy<T> caching)
47. Internal API extraction strategy (extract vs reflect for internal APIs)
AtomUI-Specific (6 categories) 48. PlacementMode usage in PopupUtils (property renamed, refactor needed) 49. IInputRoot is [PrivateApi] but still public — use directly, only use reflection for truly internal members 50. ReflectionExtensions for internal members (wrap internal/private access) 51. Windows ExtendClientAreaToDecorationsHint behavior improved 52. Popup.MotionAwareOpen/MotionAwareClose removed in AtomUI 6.0 — use Popup.IsOpen directly 53. SelectingItemsControl.UpdateSelection obsolete — prefer UpdateSelectionFromEvent, fall back to Selection.Select/Deselect for non-input events 54. SelectingItemsControl selection trigger timing changed — override ShouldTriggerSelection for custom pointer event handling
Include:
Only auto-fix safe transformations. Flag complex changes for manual review.
After applying code changes during migration, you MUST rerun full-scope scans for the critical Avalonia 12 patterns relevant to the target:
Example residual scan commands:
rg "Dispatcher\\.UIThread|MotionAwareOpen|MotionAwareClose|KeyboardNavigationHandler" src/AtomUI.Desktop.Controls
rg "PlacementMode\\s*=|Gestures\\.|BindingPlugins|DataFormats\\." src/AtomUI.Desktop.ControlsResidual scan is NOT optional — it catches:
After applying any code changes during migration, you MUST verify the changes compile successfully:
.csproj file for the module being migrateddotnet build <project-file> to verify compilationBuild verification is NOT optional — it catches:
Example build command:
dotnet build src/AtomUI.Desktop.Controls/AtomUI.Desktop.Controls.csprojIf build fails multiple times:
.referenceprojects/ (see Rule 12 for the full list: Avalonia/src, Avalonia.Controls.DataGrid, Svg.Skia/src/Svg.Controls.Avalonia, avalonia-docs)When migrating a ShowCase from release/5.0 to release/6.0, you MUST complete ALL 7 registration steps. Missing ANY step causes the ShowCase to not display or not appear in the menu.
COMPLETE REGISTRATION CHECKLIST (ALL 7 STEPS REQUIRED):
Location: controlgallery/AtomUIGallery/ShowCases/ViewModels/{Category}/
using AtomUI.Controls;
using ReactiveUI;
namespace AtomUIGallery.ShowCases.ViewModels;
public class YourViewModel : ReactiveObject, IRoutableViewModel
{
public static EntityKey ID = "YourControl";
public IScreen HostScreen { get; }
public string? UrlPathSegment => ID.ToString();
public YourViewModel(IScreen screen)
{
HostScreen = screen;
}
}IMPORTANT: ViewModel ID pattern in release/6.0:
public static EntityKey ID = "YourControl"; (NOT public const string ID)public string? UrlPathSegment => ID.ToString(); (expression-bodied property, NOT { get; } = ID;)Location: controlgallery/AtomUIGallery/ShowCases/Views/{Category}/
AXAML:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AtomUIGallery.ShowCases.Views.YourShowCase"
xmlns:atom="https://atomui.net"
xmlns:gallery="https://atomui.net/oss-controls/gallery">
<gallery:ShowCasePanel>
<!-- ShowCase items here -->
</gallery:ShowCasePanel>
</UserControl>Code-behind:
using AtomUIGallery.ShowCases.ViewModels;
using ReactiveUI;
using ReactiveUI.Avalonia;
namespace AtomUIGallery.ShowCases.Views;
public partial class YourShowCase : ReactiveUserControl<YourViewModel>
{
public YourShowCase()
{
this.WhenActivated(disposables => { });
InitializeComponent();
}
}File: controlgallery/AtomUIGallery/Workspace/ViewModels/CaseNavigationViewModel.cs
private void RegisterShowCaseViewModels()
{
// ... existing registrations ...
_showCaseViewModelFactories.Add(YourViewModel.ID, () => new YourViewModel(HostScreen));
}File: controlgallery/AtomUIGallery/ShowCases/ShowCaseRegister.cs
public void RegisterViews(DefaultViewLocator locator)
{
// ... existing mappings ...
locator.Map<YourViewModel, YourShowCase>(() => new YourShowCase());
}File: controlgallery/AtomUIGallery/Workspace/Views/CaseNavigation.axaml
Add under the appropriate category node:
<atom:NavMenuNode Header="{gallery:CaseNavigationLangResource Category_YourControl}"
ItemKey="{x:Static viewmodels:YourViewModel.ID}" />Categories:
File: controlgallery/AtomUIGallery/Workspace/Localization/CaseNavigationLang/zh_CN.cs
public const string Category_YourControl = "YourControl 中文名称";Example:
public const string Feedback_Watermark = "Watermark 水印";
public const string DataEntry_CheckBox = "CheckBox 多选框";File: controlgallery/AtomUIGallery/Workspace/Localization/CaseNavigationLang/en_US.cs
public const string Category_YourControl = "YourControl";Example:
public const string Feedback_Watermark = "Watermark";
public const string DataEntry_CheckBox = "CheckBox";VERIFICATION CHECKLIST (Check ALL before considering migration complete):
ViewModels/{Category}/Views/{Category}/CaseNavigationViewModel.csShowCaseRegister.csCaseNavigation.axaml (MOST COMMONLY FORGOTTEN)zh_CN.cs (MOST COMMONLY FORGOTTEN)en_US.cs (MOST COMMONLY FORGOTTEN)dotnet build controlgallery/AtomUIGallery.DesktopCOMMON MISTAKES AND SYMPTOMS:
| Missing Step | Symptom |
|---|---|
| Step 7.3 (ViewModel registration) | Runtime error when clicking menu item |
| Step 7.4 (View mapping) | ShowCase doesn't display, blank screen |
| Step 7.5 (Menu item) | ShowCase doesn't appear in menu at all |
| Step 7.6 (Chinese resource) | Menu shows resource key instead of Chinese text |
| Step 7.7 (English resource) | Menu shows resource key instead of English text |
CRITICAL: Steps 7.5, 7.6, and 7.7 are the MOST COMMONLY FORGOTTEN. Always double-check these three steps before considering the migration complete.
Verification: After completing ALL 7 steps, build the Gallery app and verify:
What changed: Avalonia 12 requires .NET 8+. Android/iOS require .NET 10.
Detection:
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>Fix:
<TargetFramework>net10.0</TargetFramework>What changed: All Avalonia packages must be v12.
Detection:
<PackageReference Include="Avalonia" Version="11.3.12" />Fix:
<PackageReference Include="Avalonia" Version="12.0.0" />What changed: IBinding removed, use BindingBase. InstancedBinding removed, use BindingExpressionBase.
Detection:
IBinding binding = new Binding("Property");
var instanced = new InstancedBinding(...);Fix:
BindingBase binding = new ReflectionBinding(nameof(Item.Property));
var instanced = new CompiledBinding(...);What changed: AvaloniaUseCompiledBindingsByDefault is now true by default.
Impact: All {Binding} in XAML now use compiled bindings.
Action: Verify compiled bindings work with your data context.
What changed: BindingPlugins class is now internal. Data validation plugin disabled by default. Related types removed: DataValidationBase, ExceptionValidationPlugin, IDataValidationPlugin, IndeiValidationPlugin, IPropertyAccessorPlugin, IStreamPlugin, PropertyAccessorBase, PropertyError.
Detection:
BindingPlugins.DataValidators.Add(new ExceptionValidationPlugin());Fix: Remove plugin registration. Enable validation with .WithDataAnnotationsValidation() in AppBuilder if needed.
What changed: Text shaper must be configured independently.
Detection:
AppBuilder.Configure<App>()
.UseSkia()
// Missing UseHarfBuzz()Fix:
AppBuilder.Configure<App>()
.UseSkia()
.UseHarfBuzz()Also add package:
<PackageReference Include="Avalonia.HarfBuzz" Version="12.0.0" />What changed: Touch/pen selection triggers on release, not press. Container types handle selection directly.
Detection:
protected override void UpdateSelection(ItemsControl itemsControl, int index, bool selected)
{
// Old selection logic
}Fix:
protected override void UpdateSelectionFromEvent(ItemsControl itemsControl, RoutedEventArgs e)
{
// New selection logic
}What changed: Visual.VisualRoot changed from public to protected internal (not removed, but inaccessible from outside). Use TopLevel.GetTopLevel(visual). IRenderRoot and ILayoutRoot are now truly internal. IInputRoot is marked [PrivateApi] but remains a public interface — it can and should be used directly. New IPresentationSource interface introduced (internal). KeyboardNavigationHandler is now internal — use FocusManager.GetNextElement instead.
IMPORTANT: [PrivateApi] is a documentation attribute only. It does NOT change the C# access modifier. IInputRoot is still public and fully usable. Do NOT replace IInputRoot with TopLevel — this will break code because RawInputEventArgs.Root returns PresentationSource (which implements IInputRoot), NOT TopLevel.
Detection patterns:
// Pattern 1: Direct VisualRoot access (now protected internal)
var root = visual.VisualRoot as IRenderRoot;
if (root != null) { }
// Pattern 2: IRenderRoot / ILayoutRoot usage (now truly internal)
IRenderRoot renderRoot = ...;
ILayoutRoot layoutRoot = ...;
// Pattern 3: KeyboardNavigationHandler usage (now internal)
KeyboardNavigationHandler.GetNext(element, direction)Fix:
// Pattern 1: Use TopLevel.GetTopLevel()
var topLevel = TopLevel.GetTopLevel(visual);
if (topLevel is WindowBase window) { }
// Pattern 2: Use reflection or extraction for IRenderRoot/ILayoutRoot
// See Category 35 for internal API extraction strategy
// Pattern 3: Use FocusManager instead
focusManager.TryMoveFocus(NavigationDirection.Next)NO fix needed for IInputRoot usage:
// These are ALL CORRECT in Avalonia 12 — do NOT change them:
public void Update(IInputRoot root, Visual? candidateToolTipHost) // OK
if (root == currentToolTip?.GetVisualRoot() as IInputRoot) // OK
if (e.Root == currentTip.GetVisualRoot() as IInputRoot) // OK
e.Root.GetRootElement() == _tipControl?.GetVisualRoot() // OKWhy: The visual tree architecture was refactored. TopLevel is now the primary way to access the root visual. IPresentationSource is the new internal abstraction that implements IInputRoot. Visual.VisualRoot still exists but is protected internal. IInputRoot is marked [PrivateApi] but remains public — it compiles and works correctly. IRenderRoot and ILayoutRoot are truly internal and need reflection/extraction.
What changed: Complete redesign of window decoration system. Many types removed, replaced by WindowDrawnDecorations template-based system.
Removed types:
Chrome.TitleBar classChrome.CaptionButtons classChromeOverlayLayer classLightDismissOverlayLayer classSystemDecorations enumExtendClientAreaChromeHints enumIPopupHostProvider interfaceIPopupHost interface (now internal)Removed properties:
VisualLayerManager.AdornerLayer → use AdornerLayer.GetAdornerLayer()VisualLayerManager.ChromeOverlayLayer → use WindowDrawnDecorationsVisualLayerManager.LightDismissOverlayLayer → removedVisualLayerManager.OverlayLayer → use OverlayLayer.GetOverlayLayer()Window.ExtendClientAreaChromeHints → use Window.WindowDecorations + ExtendClientAreaToDecorationsHintNew types:
WindowDrawnDecorations — template-based decoration managerWindowDrawnDecorationsContent — holds Overlay, Underlay, FullscreenPopover slotsIWindowDrawnDecorationsTemplate — template interfaceDrawnWindowDecorationParts enum — flags for Shadow, Border, TitleBar, ResizeGripsWindowDecorationsElementRole enum — roles: None, TitleBar, CloseButton, MinimizeButton, MaximizeButton, ResizeN/S/E/W/NE/NW/SE/SW, etc.WindowDecorationProperties.ElementRoleProperty — attached property for marking element rolesDetection:
<Chrome:TitleBar />
<Chrome:CaptionButtons />var layer = VisualLayerManager.ChromeOverlayLayer;
Window.ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;Fix:
<Chrome:WindowDrawnDecorations />var adorner = AdornerLayer.GetAdornerLayer(visual);
var overlay = OverlayLayer.GetOverlayLayer(visual);
// Use Window.WindowDecorations instead of ExtendClientAreaChromeHintsWhy: The old decoration system was inflexible. The new template-based system allows full customization of window chrome with explicit role-based hit testing.
What changed: Both GotFocus and LostFocus now use FocusChangedEventArgs (with NewFocusedElement, OldFocusedElement, NavigationMethod, KeyModifiers). GotFocusEventArgs class removed. KeyboardNavigationHandler is now internal — use IFocusManager.TryMoveFocus(direction, options) instead.
Detection:
protected override void OnGotFocus(GotFocusEventArgs e)
protected override void OnLostFocus(RoutedEventArgs e)
KeyboardNavigationHandler.GetNext(element, direction)Fix:
protected override void OnGotFocus(FocusChangedEventArgs e)
protected override void OnLostFocus(FocusChangedEventArgs e)
focusManager.TryMoveFocus(NavigationDirection.Next)What changed: IDataObject removed. Use IAsyncDataTransfer. Methods moved to extensions.
Detection:
var data = new DataObject();
data.Set(DataFormats.Text, "text");
await clipboard.SetDataObjectAsync(data);
var text = await clipboard.GetTextAsync();Fix:
var item = new DataTransferItem();
item.Set(DataFormat.Text, "text");
var data = new DataTransfer();
data.Add(item);
await clipboard.SetDataAsync(data);
var text = await clipboard.TryGetTextAsync();Add using:
using Avalonia.Input.Platform;What changed: DoDragDrop → DoDragDropAsync. DragEventArgs.Data → DragEventArgs.DataTransfer.
Detection:
DragDrop.DoDragDrop(dragEventArgs, dataObject);
var data = dragEventArgs.Data;Fix:
await DragDrop.DoDragDropAsync(dragEventArgs, dataTransfer);
var data = dragEventArgs.DataTransfer;What changed: DataFormats.* → DataFormat.*
Detection:
data.Set(DataFormats.Text, "text");
data.Set(DataFormats.Files, files);Fix:
data.Set(DataFormat.Text, "text");
data.Set(DataFormat.Files, files);What changed: GenericTextRunProperties, TextCollapsingProperties, TextShaperOptions merged constructors. FontFeatureCollection now last parameter.
Detection:
new GenericTextRunProperties(typeface, features, size, decorations, brush)Fix:
new GenericTextRunProperties(typeface, size, decorations, brush, fontFeatures: features)What changed: Access keys triggered by symbol, not virtual key. AccessText.AccessKey is now string? not char.
Detection:
public char AccessKey { get; set; }Fix:
public string? AccessKey { get; set; }What changed: Type 1 fonts (.pfb/.pfm) no longer supported.
Action: Use TrueType (.ttf) or OpenType (.otf) fonts instead.
What changed: Direct2D1 backend removed. Use Skia.
Detection:
<PackageReference Include="Avalonia.Direct2D1" Version="11.3.12" />Fix:
<PackageReference Include="Avalonia.Skia" Version="12.0.0" />Code:
AppBuilder.Configure<App>()
.UseSkia()What changed: AvaloniaMainActivity<TApp> → AvaloniaMainActivity + AvaloniaAndroidApplication<TApp>.
Detection:
[Activity]
public class MainActivity : AvaloniaMainActivity<App>
{
}Fix:
[Activity]
public class MainActivity : AvaloniaMainActivity
{
}
[Application]
public class AndroidApp : AvaloniaAndroidApplication<App>
{
protected AndroidApp(IntPtr javaReference, JniHandleOwnership transfer)
: base(javaReference, transfer)
{
}
}What changed: ISingleViewApplicationLifetime → IActivityApplicationLifetime with MainViewFactory.
Detection:
if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
singleView.MainView = new MainView();Fix:
if (ApplicationLifetime is IActivityApplicationLifetime activityLifetime)
activityLifetime.MainViewFactory = () => new MainView();
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
singleView.MainView = new MainView();What changed: iOS now uses scene-based lifecycle. AvaloniaAppDelegate.Window stays null.
Action: Override AvaloniaView.MovedToWindow to detect window attachment.
What changed: Avalonia.Browser.Blazor package removed. Use Avalonia.Browser.
Detection:
<PackageReference Include="Avalonia.Browser.Blazor" Version="11.3.12" />Fix:
<PackageReference Include="Avalonia.Browser" Version="12.0.0" />What changed: Tizen platform no longer supported.
Action: Migrate to supported platform or maintain custom fork.
What changed: Avalonia.Diagnostics package → AvaloniaUI.DiagnosticsSupport. The extension method AttachDevTools() may be renamed to AttachDeveloperTools() in the new package (verify with the package version you use).
Detection:
AttachDevTools();<PackageReference Include="Avalonia.Diagnostics" Version="11.x" />Fix:
// Method name depends on AvaloniaUI.DiagnosticsSupport version
AttachDeveloperTools();
// or AttachDevTools() — check the package APIPackage:
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.0" />What changed: xUnit.net v3 (from v2), NUnit v4 (from v3).
Action: Update test projects and follow official migration guides.
What changed: Screen is now abstract. Don't construct it.
Detection:
var screen = new Screen();Fix:
var screen = Screens.Primary;
var screens = Screens.All;What changed: Now a readonly record struct (was class). Use ResourcesChangedEventArgs.Create() to construct with auto-incremented sequence numbers.
Detection:
var args = new ResourcesChangedEventArgs();Fix:
var args = ResourcesChangedEventArgs.Create();What changed: Gestures class is now internal. All attached events (Holding, Tapped, RightTapped, DoubleTapped, Pinch, etc.) moved to InputElement as direct events. Remove Gestures. prefix in XAML and code.
Detection:
<Button Gestures.Pinch="Button_Pinch" />Gestures.TappedEvent
Gestures.DoubleTappedEvent
Gestures.ScrollGestureEndedEventFix:
<Button Pinch="Button_Pinch" />InputElement.TappedEvent
InputElement.DoubleTappedEvent
InputElement.ScrollGestureEndedEventWhat changed: Avalonia.Utilities.MathUtilities class is now internal. All floating-point comparison methods (AreClose, LessThan, GreaterThan, IsZero, IsOne, etc.) are no longer accessible from external assemblies.
Detection:
using Avalonia.Utilities;
if (MathUtilities.AreClose(value1, value2))
if (MathUtilities.LessThan(a, b))
if (MathUtilities.IsZero(value))Fix:
using AtomUI.Utils;
if (MathUtils.AreClose(value1, value2))
if (MathUtils.LessThan(a, b))
if (MathUtils.IsZero(value))Why: AtomUI provides AtomUI.Utils.MathUtils as a public wrapper around the internal MathUtilities class. This class contains the same floating-point comparison methods with identical epsilon-based logic. Use MathUtils instead of attempting to access the internal MathUtilities via reflection.
What changed: Now a direct property, not styled property. Can't set from style.
Action: Set WindowState in code-behind or binding, not in styles.
What changed: Data validation enabled by default for properties with enableDataValidation: true.
Action: Remove UpdateDataValidation overrides that only call DataValidationErrors.SetError.
What changed: Custom extension methods using removed or truly internal APIs need updating. Common patterns:
GetVisualRoot() extension using GetPresentationSource() (needed because Visual.VisualRoot is now protected internal)GetRootElement() method on IInputRoot (IInputRoot itself is still public and usable)internal interfaces like IRenderRoot, ILayoutRootDetection:
// In VisualExtensions.cs or similar utility files
internal static Visual? GetVisualRoot(this Visual visual)
{
return visual.GetPresentationSource()?.RootVisual;
}
// Usage in service classes — these are FINE, no migration needed:
if (Presenter?.GetVisualRoot() != null)
if (e.Root == currentTip.GetVisualRoot() as IInputRoot)Fix:
// GetVisualRoot() extension is correct as-is if it uses GetPresentationSource()
// IInputRoot usage is correct as-is — do NOT replace with TopLevel
// Only fix extensions that reference truly internal types (IRenderRoot, ILayoutRoot)
// Option 2: Replace all usages directly with TopLevel.GetTopLevel()
if (Presenter != null && TopLevel.GetTopLevel(Presenter) != null)
if (e.Root == TopLevel.GetTopLevel(currentTip))Why: GetPresentationSource() is internal/protected in Avalonia 12. TopLevel.GetTopLevel() is the public API for accessing the root. Update all extension methods to use the new API.
What changed: When accessing private/internal Avalonia APIs, use standardized ReflectionExtensions pattern with [DynamicDependency] attributes for AOT safety.
Pattern:
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using AtomUI.Reflection;
internal static class TargetClassReflectionExtensions
{
#region 反射信息定义
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, typeof(TargetClass))]
private static readonly Lazy<PropertyInfo> PropertyNamePropertyInfo = new Lazy<PropertyInfo>(() =>
typeof(TargetClass).GetPropertyInfoOrThrow("PropertyName",
BindingFlags.Instance | BindingFlags.NonPublic));
#endregion
public static PropertyType GetPropertyName(this TargetClass target)
{
var value = PropertyNamePropertyInfo.Value.GetValue(target) as PropertyType;
Debug.Assert(value != null);
return value;
}
}Key Points:
[DynamicDependency] to mark members for AOT preservationLazy<T> to cache reflection infoGetXxxInfoOrThrow() for safe reflectionDebug.Assert() for null checks{MemberName}{MemberType}InfoWhy: Prevents AOT trimming of private/internal members that are accessed via reflection. Essential for shipping AOT-compiled applications.
What changed: The Popup.PlacementMode property is renamed to Popup.Placement. The PlacementMode enum itself still exists and is unchanged. Similarly, ContextMenu.PlacementMode → ContextMenu.Placement.
Detection:
popup.PlacementMode = PlacementMode.Bottom;
contextMenu.PlacementMode = PlacementMode.Right;
// Note: PlacementMode enum usage is fine, only the property name changedFix:
popup.Placement = PlacementMode.Bottom;
contextMenu.Placement = PlacementMode.Right;
// PlacementMode enum values remain the sameXAML Detection:
<Popup PlacementMode="Bottom" />XAML Fix:
<Popup Placement="Bottom" />Why: Property renamed for consistency. The PlacementMode enum is NOT removed — only the property accessor name changed. Code that uses PlacementMode enum values directly (e.g., in switch statements, comparisons) does NOT need changes.
What changed: IInputRoot interface is now marked [PrivateApi] but remains a public interface. It is fully compilable and usable. IRenderRoot is truly internal. ILayoutRoot is truly internal. PresentationSource (internal class) implements IInputRoot and is the actual object returned by RawInputEventArgs.Root.
IMPORTANT: Do NOT replace IInputRoot usage with TopLevel. RawInputEventArgs.Root returns a PresentationSource object which implements IInputRoot but is NOT a TopLevel. Replacing IInputRoot with TopLevel will cause comparisons to always fail and break functionality (e.g., tooltips stop triggering).
No migration needed for:
// All of these are CORRECT in Avalonia 12:
public void Process(IInputRoot root) // OK — IInputRoot is public
if (root is IInputRoot inputRoot) // OK — compiles and works
e.Root == currentTip.GetVisualRoot() as IInputRoot // OK
public void Update(IInputRoot root, Visual? candidateToolTipHost) // OKOnly use reflection for truly internal members of IInputRoot:
// RootElement property is not on the public interface — needs reflection
typeof(IInputRoot).GetProperty("RootElement", ...) // Use ReflectionExtensions patternWhy: [PrivateApi] is a documentation-only attribute indicating the API may change in future versions. It does NOT change the C# access modifier. IInputRoot is still public and the correct type to use when working with RawInputEventArgs.Root. Only members that are not part of the public interface surface (like RootElement on PresentationSource) need reflection.
What changed: Some Avalonia 12 classes, structs, or interfaces are internal but required by AtomUI.
Options:
Option 1: Use ReflectionExtensions (Recommended for small APIs)
[DynamicDependency] attributesLazy<T> caching for reflection infoOption 2: Extract Code (Recommended for complex APIs)
AtomUI.Core — Core utilities, base classesAtomUI.Controls — Platform-agnostic controlsAtomUI.Desktop.Controls — Desktop-specific implementationsAtomUI.XXXinternal to avoid public API pollutionExample - Extracting Internal Struct:
// From Avalonia (internal)
namespace Avalonia.Controls.Primitives.PopupPositioning
{
internal struct PopupPositioningData
{
public Point Offset { get; set; }
public Size Size { get; set; }
}
}
// Extract to AtomUI.Controls
namespace AtomUI.Controls.Primitives
{
/// <summary>
/// Extracted from Avalonia 12 internal API for popup positioning.
/// </summary>
internal struct PopupPositioningData
{
public Point Offset { get; set; }
public Size { get; set; }
}
}When to Extract:
When to Use Reflection:
Why: Extraction avoids reflection overhead, improves AOT compatibility, and makes code more maintainable than reflection-based access.
What changed: Public classes/structs/interfaces in Avalonia 12 may have internal or private members that AtomUI needs to access.
Strategy: Use ReflectionExtensions pattern to safely wrap internal member access.
When to Use:
publicinternal or private membersPattern:
// File: {TargetClass}ReflectionExtensions.cs
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using AtomUI.Reflection;
using Avalonia.XXX;
namespace AtomUI.XXX;
/// <summary>
/// Reflection wrapper for accessing internal members of Avalonia's {TargetClass}.
/// </summary>
internal static class TargetClassReflectionExtensions
{
#region 反射信息定义
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, typeof(TargetClass))]
private static readonly Lazy<PropertyInfo> InternalPropertyPropertyInfo = new(() =>
typeof(TargetClass).GetPropertyInfoOrThrow("InternalProperty",
BindingFlags.Instance | BindingFlags.NonPublic));
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicFields, typeof(TargetClass))]
private static readonly Lazy<FieldInfo> _internalFieldFieldInfo = new(() =>
typeof(TargetClass).GetFieldInfoOrThrow("_internalField",
BindingFlags.Instance | BindingFlags.NonPublic));
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(TargetClass))]
private static readonly Lazy<MethodInfo> InternalMethodMethodInfo = new(() =>
typeof(TargetClass).GetMethodInfoOrThrow("InternalMethod",
BindingFlags.Instance | BindingFlags.NonPublic));
#endregion
/// <summary>
/// Gets the internal property value.
/// </summary>
public static PropertyType GetInternalProperty(this TargetClass target)
{
var value = InternalPropertyPropertyInfo.Value.GetValue(target) as PropertyType;
Debug.Assert(value != null);
return value;
}
/// <summary>
/// Sets the internal property value.
/// </summary>
public static void SetInternalProperty(this TargetClass target, PropertyType value)
{
InternalPropertyPropertyInfo.Value.SetValue(target, value);
}
/// <summary>
/// Gets the internal field value.
/// </summary>
public static FieldType GetInternalField(this TargetClass target)
{
var value = _internalFieldFieldInfo.Value.GetValue(target) as FieldType;
Debug.Assert(value != null);
return value;
}
/// <summary>
/// Invokes the internal method.
/// </summary>
public static ReturnType InvokeInternalMethod(this TargetClass target, ParameterType param)
{
var result = InternalMethodMethodInfo.Value.Invoke(target, [param]);
Debug.Assert(result != null);
return (ReturnType)result;
}
}Usage:
// Before: Direct reflection (unsafe, not AOT-friendly)
var prop = typeof(TargetClass).GetProperty("InternalProperty",
BindingFlags.Instance | BindingFlags.NonPublic);
var value = prop?.GetValue(target);
// After: Using ReflectionExtensions (safe, AOT-friendly)
var value = target.GetInternalProperty();Key Points:
{TargetClass}ReflectionExtensions.csinternal static class {TargetClass}ReflectionExtensions[DynamicDependency] attribute on each reflection info fieldLazy<T> for caching reflection infoDebug.Assert() for null checks{MemberName}{MemberType}Info for reflection fieldsWhy:
[DynamicDependency] prevents trimmingLazy<T> caches reflection infoExample from AtomUI:
// TextParagraphPropertiesReflectionExtensions.cs
internal static class TextParagraphPropertiesReflectionExtensions
{
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties,
typeof(TextParagraphProperties))]
private static readonly Lazy<PropertyInfo> LineSpacingPropertyInfo = new(() =>
typeof(TextParagraphProperties).GetPropertyInfoOrThrow("LineSpacing",
BindingFlags.Instance | BindingFlags.NonPublic));
public static double GetLineSpacing(this TextParagraphProperties properties)
{
var lineSpacing = LineSpacingPropertyInfo.Value.GetValue(properties) as double?;
Debug.Assert(lineSpacing != null);
return lineSpacing.Value;
}
}
// Usage
var spacing = textParagraphProperties.GetLineSpacing();What changed: Avalonia 12 supports multiple dispatchers (one per thread). Library/control authors must use AvaloniaObject.Dispatcher or Dispatcher.CurrentDispatcher instead of assuming a single global dispatcher.
Detection:
Dispatcher.UIThread.InvokeAsync(...)
Dispatcher.UIThread.Post(...)Fix:
// In control code, use the object's own dispatcher (NO this. prefix)
Dispatcher.InvokeAsync(...)
Dispatcher.Post(...)
// Or use current thread's dispatcher
Dispatcher.CurrentDispatcher.InvokeAsync(...)IMPORTANT - Code Style:
this.Dispatcher — the this. prefix is redundantthis. prefix when calling extension methods like EnableTransitions(), DisableTransitions()Dispatcher.Post(EnableTransitions)this.Dispatcher.Post(this.EnableTransitions)Note: DispatcherTimer and AvaloniaSynchronizationContext use the current dispatcher by default. Ensure instantiations happen on the correct thread or pass the target dispatcher to the constructor.
What changed: Animations no longer tick when a control is hidden (IsVisible = false).
Detection: Controls that rely on animations continuing while hidden.
Fix:
// Set PlaybackBehavior to Always to restore old behavior
animation.PlaybackBehavior = PlaybackBehavior.Always;Why: Performance optimization — hidden controls don't need animation updates.
What changed: Avalonia no longer uses .NET's BinaryFormatter for clipboard serialization. Custom objects on the clipboard must be explicitly serialized.
Detection:
// Putting custom objects on clipboard without explicit serialization
clipboard.SetDataObjectAsync(myCustomObject);Fix:
// Explicitly serialize using preferred mechanism (e.g., JSON)
var json = JsonSerializer.Serialize(myCustomObject);
await clipboard.SetTextAsync(json);Why: BinaryFormatter is deprecated in .NET for security reasons.
What changed: Multiple APIs renamed for consistency. Old names may be kept as [Obsolete] temporarily.
Renames:
| Old Name | New Name | Severity |
|---|---|---|
Popup.PlacementMode | Popup.Placement | HIGH |
ContextMenu.PlacementMode | ContextMenu.Placement | HIGH |
TextBox.Watermark | TextBox.PlaceholderText | MEDIUM |
TextBox.UseFloatingWatermark | TextBox.UseFloatingPlaceholder | MEDIUM |
Window.SystemDecorations | Window.WindowDecorations | MEDIUM |
RenderOptions.TextRenderingMode | TextOptions.TextRenderingMode | MEDIUM |
TextBlock.LetterSpacing | TextElement.LetterSpacing (attached) | MEDIUM |
Color.ToUint32() | Color.ToUInt32() (case) | LOW |
Screen.PixelDensity | Screen.Scaling | LOW |
Screen.Primary | Screen.IsPrimary | LOW |
BindingPriority.TemplatedParent | BindingPriority.Template | MEDIUM |
PseudolassesExtensions | PseudoClassesExtensions (typo) | LOW |
X11PlatformOptions.ExterinalGLibMainLoopExceptionLogger | ExternalGLibMainLoopExceptionLogger (typo) | LOW |
AttachDevTools() | AttachDeveloperTools() (verify with DiagnosticsSupport package) | MEDIUM |
Detection: Search for old names in code and XAML.
Fix: Replace with new names. Use IDE rename refactoring for safety.
What changed: 40+ members deprecated in Avalonia 11 are now removed in Avalonia 12.
Removed from Avalonia.Base:
CubicBezierEasing → use SplineEasingCustomAnimatorBase / CustomAnimatorBase<T> → use InterpolatingAnimator<T>IStyleable interface → use StyledElementRadialGradientBrush.Radius → use RadiusX and RadiusYColor.ToUint32() → use Color.ToUInt32() (case change)DrawingContext.PushPreTransform() / PushPostTransform() / PushTransformContainer() → use DrawingContext.PushTransform()AvaloniaObjectExtensions.Bind() → use AvaloniaObject.Bind()Removed from Avalonia.Controls:
IActivatableApplicationLifetime → use Application.Current.TryGetFeature<IActivatableLifetime>()FileDialog / OpenFileDialog / OpenFolderDialog / SaveFileDialog → use IStorageProviderSystemDialog class (206 lines) → use IStorageProviderItemContainerGenerator.ContainerFromIndex() / IndexFromContainer() → use ItemsControl methodsTreeContainerIndex → use TreeViewTreeItemContainerGenerator → use ItemContainerGeneratorToggleButton.Checked / Unchecked / Indeterminate events → use ToggleButton.IsCheckedChangedScreen.PixelDensity → use Screen.ScalingScreen.Primary → use Screen.IsPrimaryScreens.ScreenFromWindow() → use Screens.ScreenFromTopLevel()AppBuilder.LifetimeOverride property → removedIApplicationPlatformEvents interface → removedIInsetsManager.DisplayEdgeToEdge → use IInsetsManager.DisplayEdgeToEdgePreferenceDetection: Search for any of the above names in code.
Fix: Replace with the corresponding new API.
What changed: IPopupHost interface changed from public to internal.
Detection:
IPopupHost host = popup.Host;
if (host != null) { host.Close(); }Fix:
// Use Popup.IsOpen instead
if (popup.IsOpen)
{
popup.IsOpen = false;
}Why: Popup hosting is now an internal implementation detail. Use Popup.IsOpen for state management.
What changed: IRenderer interface is now marked [PrivateApi]. Not for public consumption.
Detection:
IRenderer renderer = topLevel.Renderer;
renderer.AddDirty(visual);Fix: Avoid direct IRenderer usage. Use higher-level APIs like InvalidateVisual() or InvalidateArrange().
What changed: VisualLayerManager is now public but layer access methods changed.
Detection:
var adorner = VisualLayerManager.AdornerLayer;
var overlay = VisualLayerManager.OverlayLayer;
var chrome = VisualLayerManager.ChromeOverlayLayer;
var dismiss = VisualLayerManager.LightDismissOverlayLayer;Fix:
var adorner = AdornerLayer.GetAdornerLayer(visual);
var overlay = OverlayLayer.GetOverlayLayer(visual);
// ChromeOverlayLayer → use WindowDrawnDecorations
// LightDismissOverlayLayer → removedWhat changed: New constructor accepting Func<IReadOnlyList<TIn?>, TOut>. Old IEnumerable<TIn?> constructor kept for backward compatibility.
Detection:
new FuncMultiValueConverter<string, string>(values => string.Join(", ", values))Fix: No change required — both constructors work. Prefer IReadOnlyList<TIn?> for new code as it provides indexed access.
What changed: New properties added to Popup control.
New properties:
OverlayDismissEventPassThrough — whether dismiss events pass through overlayOverlayInputPassThroughElement — element that receives input through overlayShouldUseOverlayLayer — whether popup should use overlay layerIsUsingOverlayLayer — read-only, whether popup is currently using overlayImpact: These provide more control over popup overlay behavior. Review existing popup customizations.
What changed: Virtual methods CreateAppBuilder() and CustomizeAppBuilder(AppBuilder) removed from AvaloniaMainActivity.
Detection:
public class MainActivity : AvaloniaMainActivity<App>
{
protected override AppBuilder CreateAppBuilder() => ...;
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) => ...;
}Fix: Move logic to AvaloniaAndroidApplication<TApp> subclass or App class.
What changed: Major rework of rendering interfaces. Only affects custom rendering backend implementations.
Key changes:
IRenderTarget.CreateDrawingContext now takes RenderTargetSceneInfo parameterIRenderTargetBitmapImpl no longer extends IRenderTargetIDrawingContextLayerImpl no longer extends IRenderTargetBitmapImplIPlatformRenderSurface instead of IEnumerable<object>ISkiaGpu now internalIRenderTarget2, ISkiaGpuRenderTarget2)ILockedFramebuffer now includes AlphaFormat propertyLockedFramebuffer constructor requires AlphaFormat parameterBitmap.CopyPixels() no longer accepts AlphaFormat parameterImpact: Only affects code implementing custom rendering backends. Standard Avalonia usage is unaffected.
What changed: ExtendClientAreaToDecorationsHint now works correctly in all scenarios on Windows.
Action: Remove previous workarounds (margin adjustments, manual offset calculations) that compensated for the old buggy behavior.
What changed: Dispatcher.InvokeAsync now captures and flows execution context from the caller (AsyncLocal, impersonation, culture).
Impact: Most async usages now behave as expected. Code that relied on execution context NOT flowing may need adjustment.
What changed: AccessText.AccessKey property type changed from char to string?. Access keys now triggered by printed symbol, not virtual key. Accented characters and numbers now work as access keys.
Detection:
char key = accessText.AccessKey;Fix:
string? key = accessText.AccessKey;# Avalonia 12 Migration Report
**Total Issues Found:** 45
**Critical Issues:** 8
**High Severity:** 15
**Medium Severity:** 18
**Low Severity:** 4
**Auto-fixable:** 12
## Platform Analysis
- Desktop: 35 issues
- Android: 5 issues
- iOS: 2 issues
- Browser: 1 issue
- Headless: 2 issues
## CRITICAL Issues (Must Fix)
### .NET Version
**File:** AtomUIV6.csproj
**Current:** net8.0
**Suggested:** net10.0
**Auto-fixable:** Yes
...dotnet build <project-file> to verify compilationAlways use braces {} for code blocks, even single-line statements:
// ❌ Wrong - no braces
if (condition)
DoSomething();
for (int i = 0; i < count; i++)
Process(i);
// ✅ Correct - always use braces
if (condition)
{
DoSomething();
}
for (int i = 0; i < count; i++)
{
Process(i);
}Why: Prevents bugs from accidental statement misalignment, improves readability, and maintains consistency with C# style guidelines.
<T>public with [PrivateApi]dotnet build verification after code changesWhen migrating code that accesses internal/private Avalonia members, always check this catalog first and use existing extensions instead of writing new reflection code.
AtomUI.Reflection namespace)| File | Class | Description |
|---|---|---|
src/AtomUI.Core/Reflection/TypeMemberExtension.cs | TypeMemberExtension | Safe reflection helpers: TryGetPropertyInfo, TryGetFieldInfo, TryGetMethodInfo, TryGetEventInfo, and *OrThrow variants |
src/AtomUI.Core/Reflection/ObjectExtension.cs | ObjectExtension | Instance-level reflection: TryGetProperty<T>, GetPropertyOrThrow<T>, TrySetProperty<T>, TryGetField<T>, GetFieldOrThrow<T>, TrySetField<T>, TryInvokeMethod, InvokeMethodOrThrow |
| File | Class | Extension Method | Target Type | Wrapped Member | Access |
|---|---|---|---|---|---|
src/AtomUI.Core/Input/IInputRootRefectionExtensions.cs | IInputRootReflectionExtensions | GetRootElement(this IInputRoot) | IInputRoot | RootElement property | NonPublic |
src/AtomUI.Core/Utils/AvaloniaPropertyReflectionExtensions.cs | AvaloniaPropertyReflectionExtensions | InvokeNotifying(this AvaloniaProperty, AvaloniaObject, bool) | AvaloniaProperty | Notifying property (delegate) | NonPublic |
src/AtomUI.Core/Controls/VisualReflectionExtensions.cs | VisualReflectionExtensions | SetVisualParent(this Visual, Control?) | Visual | SetVisualParent() method | NonPublic |
ClearVisualParentRecursive(this Visual) | Visual | recursive SetVisualParent(null) | NonPublic | ||
GetVisualChildrenList(this Visual) | Visual | VisualChildren property → IAvaloniaList<Visual> | NonPublic | ||
IndexOfVisualChildren(this Visual, Visual) | Visual | via GetVisualChildrenList | NonPublic | ||
AddToVisualChildren(this Visual, Visual) | Visual | via GetVisualChildrenList | NonPublic | ||
InsertToVisualChildren(this Visual, int, Control) | Visual | via GetVisualChildrenList | NonPublic | ||
src/AtomUI.Core/Controls/ItemCollectionReflectionExtensions.cs | ItemCollectionReflectionExtensions | SetItemsSource(this ItemCollection, IEnumerable?) | ItemCollection | SetItemsSource() method | NonPublic |
src/AtomUI.Core/Controls/RawPointerEventTypeReflectionExtensions.cs | RawPointerEventTypeReflectionExtensions | GetInputHitTestResult(this RawPointerEventArgs) | RawPointerEventArgs | InputHitTestResult property | NonPublic |
src/AtomUI.Core/Animations/AnimatableReflectionExtensions.cs | AnimatableReflectionExtensions | EnableTransitions(this Animatable) | Animatable | EnableTransitions() method | NonPublic |
DisableTransitions(this Animatable) | Animatable | DisableTransitions() method | NonPublic | ||
src/AtomUI.Core/Reflection/StyledElementReflectionExtensions.cs | StyledElementReflectionExtensions | GetLogicalChildrenList(this StyledElement) | StyledElement | LogicalChildren property → IAvaloniaList<ILogical> | NonPublic |
AddToLogicalChildren(this StyledElement, ILogical) | StyledElement | via GetLogicalChildrenList | NonPublic | ||
InsertToLogicalChildren(this StyledElement, int, Control) | StyledElement | via GetLogicalChildrenList | NonPublic | ||
SetTemplatedParent(this StyledElement, AvaloniaObject?) | StyledElement | TemplatedParent property setter | Public | ||
SetTemplatedParentRecursive(this StyledElement, AvaloniaObject?) | StyledElement | recursive SetTemplatedParent | Public | ||
src/AtomUI.Core/Media/TextFormatting/TextParagraphPropertiesReflectionExtensions.cs | TextParagraphPropertiesReflectionExtensions | GetLineSpacing(this TextParagraphProperties) | TextParagraphProperties | LineSpacing property | NonPublic |
SetLineSpacing(this TextParagraphProperties, double) | TextParagraphProperties | LineSpacing property | NonPublic | ||
src/AtomUI.Core/Data/DynamicResourceReflectionExtension.cs | DynamicResourceReflectionExtension | SetAnchor(this DynamicResourceExtension, object?) | DynamicResourceExtension | _anchor field | NonPublic |
src/AtomUI.Core/Controls/VisualExtensions.cs | VisualExtensions | GetVisualRoot(this Visual) | Visual | GetPresentationSource()?.RootVisual | protected internal |
| File | Class | Extension Method | Target Type | Wrapped Member | Access |
|---|---|---|---|---|---|
src/AtomUI.Controls/ItemsControl/ItemCollectionReflectionExtensions.cs | ItemCollectionReflectionExtensions | AddSourceChangedEvent(this ItemCollection, EventHandler?) | ItemCollection | SourceChanged event (add) | NonPublic |
src/AtomUI.Controls/ItemsControl/ItemsControlReflectionExtensions.cs | ItemsControlReflectionExtensions | GetWrapFocus(this ItemsControl) | ItemsControl | WrapFocus property | NonPublic |
SetWrapFocus(this ItemsControl, bool) | ItemsControl | WrapFocus property | NonPublic | ||
GetItems(this ItemsControl) | ItemsControl | _items field | NonPublic | ||
src/AtomUI.Controls/ItemsControl/ItemsSourceViewReflectionExtensions.cs | ItemsSourceViewReflectionExtensions | TryGetInitializedSource(this ItemsSourceView) | ItemsSourceView | TryGetInitializedSource() method | NonPublic |
src/AtomUI.Controls/Primitives/TextSearchReflectionExtensions.cs | TextSearchUtils | GetEffectiveText(object?, BindingEvaluator<string?>?) | TextSearch | GetEffectiveText() static method | NonPublic |
src/AtomUI.Controls/Primitives/TopLevelReflectionExtensions.cs | TopLevelReflectionExtensions | GetLastPointerPosition(this TopLevel) | TopLevel | LastPointerPosition property | NonPublic |
src/AtomUI.Controls/Primitives/VisualLayers/VisualLayerManagerReflectionExtensions.cs | VisualLayerManagerReflectionExtensions | AddLayer(this VisualLayerManager, Control, int) | VisualLayerManager | AddLayer() method | NonPublic |
GetLayers(this VisualLayerManager) | VisualLayerManager | _layers field | NonPublic | ||
src/AtomUI.Controls/ScrollViewer/ScrollBarReflectionExtensions.cs | ScrollBarReflectionExtensions | GetTimer(this ScrollBar) | ScrollBar | _timer field | NonPublic |
SetIsExpanded(this ScrollBar, bool) | ScrollBar | IsExpanded property (private setter) | Public/Private |
| File | Class | Extension Method | Target Type | Wrapped Member | Access |
|---|---|---|---|---|---|
src/AtomUI.Desktop.Controls/Popup/PopupReflectionExtensions.cs | PopupReflectionExtensions | AddClosingEventHandler(this Popup, EventHandler<CancelEventArgs>) | Popup | Closing event (add) | NonPublic |
RemoveClosingEventHandler(this Popup, EventHandler<CancelEventArgs>) | Popup | Closing event (remove) | NonPublic | ||
SetIgnoreIsOpenChanged(this Popup, bool) | Popup | _ignoreIsOpenChanged field | NonPublic | ||
GetIgnoreIsOpenChanged(this Popup) | Popup | _ignoreIsOpenChanged field | NonPublic | ||
SetPopupParent(this Popup, Control?) | Popup | SetPopupParent() method | NonPublic | ||
src/AtomUI.Desktop.Controls/TextBlock/TextBlockReflectionExtensions.cs | TextBlockReflectionExtensions | GetMaxSizeFromConstraint(this TextBlock) | TextBlock | GetMaxSizeFromConstraint() method | NonPublic |
GetHasComplexContent(this TextBlock) | TextBlock | HasComplexContent property | NonPublic |
When you encounter code accessing an internal/private member of an Avalonia type, use this index:
| Avalonia Type | Available Extensions | Using Directive |
|---|---|---|
IInputRoot | GetRootElement() | using AtomUI.Input; |
AvaloniaProperty | InvokeNotifying() | using AtomUI.Utils; |
Visual | SetVisualParent(), GetVisualChildrenList(), AddToVisualChildren(), InsertToVisualChildren(), IndexOfVisualChildren(), ClearVisualParentRecursive(), GetVisualRoot() | using AtomUI.Controls; |
StyledElement | GetLogicalChildrenList(), AddToLogicalChildren(), InsertToLogicalChildren(), SetTemplatedParent(), SetTemplatedParentRecursive() | using AtomUI.Reflection; |
Animatable | EnableTransitions(), DisableTransitions() | using AtomUI.Animations; |
ItemCollection | SetItemsSource(), AddSourceChangedEvent() | using AtomUI.Controls; |
ItemsControl | GetWrapFocus(), SetWrapFocus(), GetItems() | using AtomUI.Controls; |
ItemsSourceView | TryGetInitializedSource() | using AtomUI.Controls; |
RawPointerEventArgs | GetInputHitTestResult() | using AtomUI.Controls; |
TextSearch | TextSearchUtils.GetEffectiveText() | using AtomUI.Controls; |
TopLevel | GetLastPointerPosition() | using AtomUI.Controls.Primitives; |
VisualLayerManager | AddLayer(), GetLayers() | using AtomUI.Controls.Primitives; |
ScrollBar | GetTimer(), SetIsExpanded() | using AtomUI.Controls.Commons; |
Popup | AddClosingEventHandler(), RemoveClosingEventHandler(), SetIgnoreIsOpenChanged(), GetIgnoreIsOpenChanged(), SetPopupParent() | using AtomUI.Desktop.Controls; |
TextBlock | GetMaxSizeFromConstraint(), GetHasComplexContent() | using AtomUI.Desktop.Controls; |
TextParagraphProperties | GetLineSpacing(), SetLineSpacing() | using AtomUI.Media.TextFormatting; |
DynamicResourceExtension | SetAnchor() | using AtomUI.Data; |
What changed: In AtomUI 6.0, the custom Popup.MotionAwareOpen() and Popup.MotionAwareClose() methods are removed. Popup open/close is now controlled directly via Popup.IsOpen.
Detection:
Popup.MotionAwareOpen(() => { HandlePopupOpened(placementTarget); });
Popup.MotionAwareClose(HandlePopupClosed);Fix:
Popup.IsOpen = true;
HandlePopupOpened(placementTarget);
// For close:
Popup.IsOpen = false;
HandlePopupClosed();Why: The motion/animation system for popups was redesigned in AtomUI 6.0. Open/close animations are now handled internally by the Popup infrastructure, so callers no longer need to use motion-aware wrappers.
What changed: SelectingItemsControl.UpdateSelection(Control, bool, bool, bool, bool, bool) and UpdateSelectionFromEventSource(...) are marked [Obsolete] in Avalonia 12. The recommended replacement is UpdateSelectionFromEvent(Control, RoutedEventArgs).
However, UpdateSelectionFromEvent only handles three event types via an internal switch:
PointerEventArgs (with ShouldTriggerSelection check)KeyEventArgs (with ShouldTriggerSelection check)FocusChangedEventArgsFor any other event type (e.g., property change events like IsCheckedChanged), UpdateSelectionFromEvent returns false. In these scenarios, fall back to the Selection model directly.
Priority: Always try UpdateSelectionFromEvent first. Only use Selection.Select/Deselect when the event is not a pointer, key, or focus event.
Detection:
UpdateSelection(container, select, rangeModifier, toggleModifier);
UpdateSelectionFromEventSource(eventSource, select, rangeModifier, toggleModifier);Fix — for pointer/key/focus events (use UpdateSelectionFromEvent):
UpdateSelectionFromEvent(container, eventArgs);Fix — for programmatic selection (use Selection model directly):
var index = IndexFromContainer(container);
if (index != -1)
{
if (shouldSelect)
{
Selection.Select(index);
}
else
{
Selection.Deselect(index);
}
}Why: Avalonia 12 redesigned selection handling to be event-driven. UpdateSelectionFromEvent extracts modifier keys from the event args to determine range/toggle behavior. When selection is driven by a non-input event (e.g., a checkbox toggling its IsChecked property), the Selection model's Select/Deselect methods are the correct API — they bypass modifier logic entirely and directly update the selection state.
What changed: When working with Popup-based controls (Menu, MenuItem, ContextMenu, Flyout), understanding the difference between Visual Tree and Logical Tree is critical for correct parent-child relationship checks.
The Problem:
IsVisualAncestorOf): Used for rendering and layout. When a Popup opens, it creates a separate PopupHost in the visual tree, breaking the visual parent-child relationship.IsLogicalAncestorOf): Used for control relationships, data binding, and event routing. MenuItem's submenu items remain logical children even when displayed in a separate Popup.Common Bug Pattern:
// WRONG: This fails for nested popups/submenus
if (popupChild.IsVisualAncestorOf(element))
{
// This returns false for submenu items because they're in a different PopupHost
}Correct Pattern:
// CORRECT: Use logical tree for control hierarchy checks
if (popupChild.IsLogicalAncestorOf(element))
{
// This correctly identifies submenu items as descendants
}Real-World Example: When implementing hover behavior for dropdown menus with submenus:
IsVisualAncestorOf will fail because submenus are in separate PopupHostsIsLogicalAncestorOf correctly identifies all menu items in the hierarchyDetection:
// Look for visual tree checks on Popup.Child or Menu/MenuItem hierarchies
popup.Child.IsVisualAncestorOf(element)
menuItem.IsVisualAncestorOf(submenuItem)Fix:
// Use logical tree for control relationship checks
popup.Child.IsLogicalAncestorOf(element)
menuItem.IsLogicalAncestorOf(submenuItem)Why: Avalonia's Popup architecture creates visual isolation (separate PopupHost) but maintains logical relationships. For control hierarchy checks (hit testing, scope validation, parent-child relationships), always use the Logical Tree. Only use Visual Tree for rendering-specific operations (layout, transforms, visual effects).
Key Takeaway: When debugging "element not found in hierarchy" issues with Popups/Menus, first check if you're using the wrong tree traversal method.
For detailed information, see references/ directory:
missed-breaking-changes.md — Recently discovered breaking changes and correctionscode-level-analysis.md — Code-level analysis from official Avalonia source (11.3.14 → 12.0.0)reflection-extensions-pattern.md — AOT-safe ReflectionExtensions pattern and best practicesavalonia12-breaking-changes.md — All 50+ breaking changes with severity classificationmigration-examples.md — Before/after code examples for each categoryatomui-migration-guide.md — AtomUI-specific patterns and migration workflow© AtomUI, LGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 7 other files (references) in .agents/skills/migrate-to-avalonia12 of AtomUI/AtomUI.
Open the folder on GitHubat commit e82b36c
Migrate To Avalonia12 next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Migrate To Avalonia12 this skillAtomUI/AtomUI | 842 | — | ~24k | Automated safety check: Warn | LGPL-3.0 | |
| Winforms To Avaloniatautcony/ChapterTool | 113 | — | ~1.2k | Automated safety check: Pass | GPL-3.0 | |
| Mvvm Toolkit Digithub/awesome-copilot | 40k | 1 repos | ~2.3k | Automated safety check: Pass | MIT | |
| Speckit ConstitutionWeihanLi/WeihanLi.Common | 242 | 11 repos | ~2.1k | Automated safety check: Pass | Apache-2.0 | |
| Update .NET OS Packagesdotnet/core | 22k | — | ~2.3k | Automated safety check: Pass | MIT | |
| Microsoft Code ReferenceMicrosoftDocs/mcp | 1.9k | 3 repos | ~1.1k | Automated safety check: Pass | CC-BY-4.0 |
tautcony/ChapterTool
Orchestrate a phased WinForms-to-Avalonia migration: behavior contracts, UI-independent application logic, platform adapters, MVVM shell, parity/cutover, evolution, and hardening.
github/awesome-copilot
Wire CommunityToolkit.Mvvm ViewModels into Microsoft.Extensions.DependencyInjection.
WeihanLi/WeihanLi.Common
Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
dotnet/core
Audits and updates os-packages.json files listing the Linux packages each .NET release needs per distro, then regenerates the Markdown from the JSON.
MicrosoftDocs/mcp
Find working code samples, verify API signatures, and fix Microsoft SDK errors using official docs.
dotnet/core
Audits and updates the supported-os.json files for .NET releases, checking them against upstream lifecycle data and regenerating the markdown with the release-notes tool.
AtomUI/AtomUI
A skill your agent uses when building, launching, debugging, or visually inspecting AtomUIGallery.Desktop from an AtomUI checkout where installed or mounted Gallery apps may share its name or bundle…
AtomUI/AtomUI
Generate a single-line commit message for AtomUI by reading the project's git staged area and recent commit style.
AtomUI/AtomUI
A skill your agent uses when optimizing, refactoring, reviewing, or fixing AtomUI controls, including control API contracts, member layout, file splitting, lifecycle, AXAML structure, correctness…
AtomUI/AtomUI
A skill your agent uses when creating, completing, splitting, reviewing, or synchronizing AtomUI control documentation under docs/controls, including overview.md, implementation.md, token.md…
AtomUI/AtomUI
A skill your agent uses when changing AtomUI or Avalonia resource bindings, DynamicResource, TokenResourceBinder, non-Visual AvaloniaObject lifecycle, IResourceHost/IThemeVariantHost…
AtomUI/AtomUI
A skill your agent uses when upgrading AtomUI NuGet dependencies, source dependency projects, ReactiveUI, Avalonia, Splat, or any third-party version where compatibility must be evaluated before…
Categories
Comprehensive Avalonia 12 migration tool for AtomUI. An agent skill from AtomUI/AtomUI. Migrate To Avalonia12 is an agent skill from AtomUI/AtomUI. Comprehensive Avalonia 12 migration tool for AtomUI.
Migrate To Avalonia12 fits situations like: migrating projects to Avalonia 12; checking compatibility.
Run `npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a claude-code`. Or copy the skill folder (.agents/skills/migrate-to-avalonia12 in AtomUI/AtomUI) into .claude/skills/migrate-to-avalonia12 in your project. Claude Code loads it when a task matches its description.
Run `npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a codex`. Or copy the skill folder (.agents/skills/migrate-to-avalonia12 in AtomUI/AtomUI) into .agents/skills/migrate-to-avalonia12 in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add AtomUI/AtomUI --skill migrate-to-avalonia12 -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/migrate-to-avalonia12, .gemini/skills/migrate-to-avalonia12, .github/skills/migrate-to-avalonia12 and .opencode/skills/migrate-to-avalonia12 in your project.
Going by SKILL.md and its folder, Migrate To Avalonia12 needs the command-line tools its instructions call (dotnet and rg).
SKILL.md names 3 domains. In commands or code: atomui.net, github.com and schemas.microsoft.com; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md flagged 1 warning(s): contains instruction-override wording (e.g. “without asking the user”). Read the flagged lines before installing; the check is not a guarantee either way.
Migrate To Avalonia12 is published under the LGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 24k tokens (SKILL.md is roughly 95k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 19k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Migrate To Avalonia12: Winforms To Avalonia (tautcony/ChapterTool, 113 stars), Mvvm Toolkit Di (github/awesome-copilot, 40k stars), Speckit Constitution (WeihanLi/WeihanLi.Common, 242 stars) and Update .NET OS Packages (dotnet/core, 22k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
AtomUI (a GitHub organization) maintains it in AtomUI/AtomUI, which has 842 GitHub stars. The repository holds 15 skills in this directory. The repository was last updated on October 7, 2026.
Source: AtomUI/AtomUI on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.