avalonia
Skill808 starsChanged 4 months agoMITView on GitHub
Avalonia UI 12 for cross-platform .NET desktop apps: compiled bindings (x:DataType, on by default in 12), StyledProperty/DirectProperty, custom and templated controls, ControlThemes, DataTemplates/ViewLocator, Generic Host + DI, StorageProvider/Clipboard, Native AOT/trimming, and packaging. Load when the user mentions "Avalonia", "AXAML", "x:DataType", "compiled binding", "StyledProperty", "TemplatedControl", "ViewLocator", or "cross-platform desktop".
2 other projects use a version of this.
Adopted by
Most recently taken up by
- linuxdevel/Avalonia-skillstheir own version, first seen Nov 6, 2026
- enigmalibs/ClaudeSkillstheir own version, first seen Nov 1, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- linuxdevel/Avalonia-skills★ 50same name
- enigmalibs/ClaudeSkills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository57 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .claude/rules/agents.mdAGENTS.md
- .codex/AGENTS.mdAGENTS.md
- .opencode/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/dotnet-rules.mdCursor rule
- api-versioningSkill
- arch-checkSkill
- architecture-advisorSkill
- aspireSkill
- authenticationSkill
- build-fixSkill
- cachingSkill
- checkpointSkill
- ci-cdSkill
- clean-architectureSkill
- code-reviewSkill
- configurationSkill
- console-cliSkill
- container-publishSkill
- convention-learnerSkill
- dddSkill
- dependency-injectionSkill
- desktop-mvvmSkill
- de-sloppifySkill
- dockerSkill
- dotnet-initSkill
- ef-coreSkill
- error-handlingSkill
- health-checkSkill
- httpclient-factorySkill
- instinct-systemSkill
- loggingSkill
- messagingSkill
- migrateSkill
- minimal-apiSkill
- modern-csharpSkill
- native-aotSkill
- openapiSkill
- opentelemetrySkill
- outdatedSkill
- planSkill
- project-setupSkill
- project-structureSkill
- resilienceSkill
- scaffoldSkill
- scalarSkill
- security-scanSkill
- serilogSkill
- specSkill
- tddSkill
- testingSkill
- token-reportSkill
- verifySkill
- vertical-sliceSkill
- workflow-masterySkill
- wrap-upSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 244
Don't load XAML at runtime (AvaloniaRuntimeXamlLoader).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Avalonia UI (v12)
Verified against Avalonia 12.1.x. Avalonia 12 requires .NET 8+ (.NET 10 recommended) and makes compiled bindings the default. Pair this skill with desktop-mvvm for view-model patterns.
Core Principles
- Compiled bindings everywhere — In Avalonia 12,
{Binding}in XAML compiles by default (AvaloniaUseCompiledBindingsByDefault=true). Every view andDataTemplateneedsx:DataType. Binding typos become build errors instead of silent blank fields, and there's no reflection, so trimming and AOT keep working. - Views are mapped to view models at compile time — Use typed
DataTemplates, or a dictionary-basedViewLocator. The template's reflectionViewLocator(Type.GetType+Activator.CreateInstance) breaks under trimming. UserControlfor composition,TemplatedControlfor reusable controls — A lookless templated control can be restyled by consumers. AUserControlis just a reusable chunk of a screen.- Platform services come from
TopLevel— UseTopLevel.GetTopLevel(visual)to get the file picker (StorageProvider),Clipboard, andLauncher. These work on Windows, macOS, Linux, and browser. Never reach for Win32/WinForms dialogs. - Build and test on every target OS — Fonts, DPI, window chrome, and file paths differ by platform. A CI matrix (windows/macos/ubuntu) catches problems before users do.
Patterns
Startup with Generic Host
internal static class Program
{
[STAThread]
public static void Main(string[] args) =>
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
// Must stay public static: the XAML previewer calls it
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace();
}
public sealed partial class App : Application
{
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
{
var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings
{
ContentRootPath = AppContext.BaseDirectory
});
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<MainWindowViewModel>();
builder.Services.AddTransient<OrdersViewModel>();
var host = builder.Build();
host.Start();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = host.Services.GetRequiredService<MainWindowViewModel>()
};
desktop.Exit += (_, _) => host.Dispose();
}
base.OnFrameworkInitializationCompleted();
}
}Why: OnFrameworkInitializationCompleted is the composition root, the point where the platform is ready but no window exists yet. The IClassicDesktopStyleApplicationLifetime check keeps the same App usable on mobile and browser lifetimes. If you call UseSkia() explicitly and see "No text shaping system configured", add Avalonia.HarfBuzz and .UseHarfBuzz(), a 12.0 change.
Compiled Bindings with x:DataType
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Contoso.Desktop.ViewModels"
x:Class="Contoso.Desktop.Views.OrdersView"
x:DataType="vm:OrdersViewModel">
<DockPanel>
<TextBox DockPanel.Dock="Top" Text="{Binding CustomerName}" PlaceholderText="Customer" />
<Button DockPanel.Dock="Top" Content="Save" Command="{Binding SaveCommand}" />
<ListBox ItemsSource="{Binding Orders}" SelectedItem="{Binding SelectedOrder}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:OrderRow">
<TextBlock Text="{Binding Number}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</UserControl>Why: the XAML compiler resolves CustomerName against OrdersViewModel at build time. Each DataTemplate changes the data context, so it declares its own x:DataType. In 12, TextBox.Watermark was renamed PlaceholderText. Use {ReflectionBinding} only for a genuinely dynamic path, and treat each one as a trimming liability.
Mapping View Models to Views (Trim-Safe)
<!-- App.axaml: typed templates, resolved at compile time -->
<Application.DataTemplates>
<DataTemplate DataType="vm:OrdersViewModel"><views:OrdersView /></DataTemplate>
<DataTemplate DataType="vm:SettingsViewModel"><views:SettingsView /></DataTemplate>
</Application.DataTemplates>// If you prefer a ViewLocator: explicit factories, no reflection
public sealed class ViewLocator : IDataTemplate
{
private static readonly Dictionary<Type, Func<Control>> Views = new()
{
[typeof(OrdersViewModel)] = () => new OrdersView(),
[typeof(SettingsViewModel)] = () => new SettingsView(),
};
public Control? Build(object? param) =>
param is not null && Views.TryGetValue(param.GetType(), out var factory)
? factory()
: new TextBlock { Text = $"No view for {param?.GetType().Name}" };
public bool Match(object? data) => data is ObservableObject;
}Why: <ContentControl Content="{Binding Current}" /> (see navigation in desktop-mvvm) then renders the right view. The template's name-convention locator compiles but fails at runtime once trimmed, because the trimmer removes "unused" view types.
StyledProperty vs DirectProperty
public sealed class Badge : TemplatedControl
{
// StyledProperty: settable from styles/themes, supports inheritance and animation
public static readonly StyledProperty<string?> TextProperty =
AvaloniaProperty.Register<Badge, string?>(nameof(Text));
public static readonly StyledProperty<IBrush?> AccentProperty =
AvaloniaProperty.Register<Badge, IBrush?>(nameof(Accent));
// DirectProperty: plain backing field, cheap, NOT settable from styles — use for read-only state
public static readonly DirectProperty<Badge, bool> IsEmptyProperty =
AvaloniaProperty.RegisterDirect<Badge, bool>(nameof(IsEmpty), o => o.IsEmpty);
private bool isEmpty = true;
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public IBrush? Accent
{
get => GetValue(AccentProperty);
set => SetValue(AccentProperty, value);
}
public bool IsEmpty
{
get => isEmpty;
private set => SetAndRaise(IsEmptyProperty, ref isEmpty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TextProperty)
{
IsEmpty = string.IsNullOrEmpty(Text);
PseudoClasses.Set(":empty", IsEmpty);
}
}
}Why: styled properties go through Avalonia's value-priority system (local value > style > theme > inherited > default), which costs a little but enables theming. Direct properties skip that system. Pseudo-classes such as :empty let themes style a state without extra properties.
Templated Control with a ControlTheme
<!-- Themes/Badge.axaml, merged into App.axaml resources -->
<ControlTheme x:Key="{x:Type local:Badge}" TargetType="local:Badge">
<Setter Property="Accent" Value="{DynamicResource SystemAccentColor}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_Border" Background="{TemplateBinding Accent}" CornerRadius="8" Padding="6,2">
<TextBlock Text="{TemplateBinding Text}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:empty /template/ Border#PART_Border">
<Setter Property="IsVisible" Value="False" />
</Style>
</ControlTheme>// Same Badge class as above, shown here for the template part only
[TemplatePart("PART_Border", typeof(Border))]
public sealed class Badge : TemplatedControl
{
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var border = e.NameScope.Find<Border>("PART_Border"); // may be null if a consumer re-templated
}
}Why: a ControlTheme keyed by type is the control's default look, and consumers replace it without subclassing. TemplateBinding is the cheap one-way link from template to control. Always tolerate a missing PART_ because consumers are free to re-template.
Cross-Platform Services via TopLevel
public sealed class AvaloniaDialogService(Func<TopLevel> topLevel) : IFileDialogService
{
public async Task<Stream?> OpenCsvAsync()
{
var files = await topLevel().StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Import orders",
AllowMultiple = false,
FileTypeFilter = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }]
});
return files is [var file] ? await file.OpenReadAsync() : null;
}
}Why: IStorageProvider works on every platform, including sandboxed macOS and the browser, where raw paths aren't available. The old OpenFileDialog/SaveFileDialog classes were removed in 12. Get the TopLevel with TopLevel.GetTopLevel(control), since casting the visual root to TopLevel no longer works in 12.
Native AOT and Trimming
<PropertyGroup>
<PublishAot>true</PublishAot> <!-- app project only -->
<IsAotCompatible>true</IsAotCompatible> <!-- every project, enables the analyzers -->
</PropertyGroup>Checklist:
- Compiled bindings only (12 default); audit any
{ReflectionBinding}andx:CompileBindings="False" - Trim-safe view mapping (typed
DataTemplates or the dictionaryViewLocator) - CommunityToolkit.Mvvm generators (no reflection), source-generated JSON (
native-aotskill) - Don't load XAML at runtime (
AvaloniaRuntimeXamlLoader); addTrimmerRootAssemblyonly for assemblies you create controls from dynamically
Why: Avalonia supports Native AOT, which gives fast cold start and no runtime install, a real advantage over WPF. But one reflection binding or locator becomes a runtime crash that only shows up in the published build. Always smoke-test the AOT output.
Packaging
dotnet publish src/Contoso.Desktop -c Release -r win-x64
dotnet publish src/Contoso.Desktop -c Release -r osx-arm64
dotnet publish src/Contoso.Desktop -c Release -r linux-x64Why: publish per RID on that OS (Native AOT needs the native toolchain of the target OS). macOS needs an .app bundle (Info.plist + icon) plus signing and notarization. On Windows, sign the exe. For installers and auto-update across all three OSes, use a dedicated tool such as Velopack rather than hand-rolled update code.
Anti-patterns
Bindings Without x:DataType
<!-- BAD — 12 compiles bindings by default: no x:DataType = build error; ReflectionBinding hides typos -->
<TextBlock Text="{ReflectionBinding CustmerName}" />
<!-- GOOD — typed, validated at build time -->
<UserControl x:DataType="vm:OrdersViewModel">
<TextBlock Text="{Binding CustomerName}" />
</UserControl>Bindings in C# Using the Binding Class
// BAD — in Avalonia 12, Binding always means ReflectionBinding
textBlock.Bind(TextBlock.TextProperty, new Binding("CustomerName"));
// GOOD — explicit compiled binding
textBlock.Bind(TextBlock.TextProperty,
CompiledBinding.Create((OrdersViewModel vm) => vm.CustomerName));Platform-Specific APIs in Shared UI Code
// BAD — Windows-only, crashes on macOS/Linux
System.Diagnostics.Process.Start("explorer.exe", folder);
// GOOD — launcher from TopLevel (desktop OSes; sandboxed targets need IStorageFolder overloads)
await TopLevel.GetTopLevel(this)!.Launcher.LaunchDirectoryInfoAsync(new DirectoryInfo(folder));Touching Controls from Background Threads
// BAD — throws: call from invalid thread
_ = Task.Run(() => statusText.Text = "Done");
// GOOD — bind to a view model property and await the work; or dispatch explicitly
await Dispatcher.UIThread.InvokeAsync(() => Status = "Done");In Avalonia 12 each thread can have its own dispatcher. App code with a single UI thread can use Dispatcher.UIThread, but reusable control libraries should use the control's Dispatcher property.
Decision Guide
| Scenario | Recommendation |
| New cross-platform desktop app | Avalonia 12 + CommunityToolkit.Mvvm + Generic Host |
| Binding a view | x:DataType on the root and on every DataTemplate |
| Truly dynamic binding path | {ReflectionBinding}, isolated and not in AOT builds |
| VM → View mapping | Typed DataTemplates in App.axaml (or dictionary ViewLocator) |
| Reusable, restylable control | TemplatedControl + ControlTheme + StyledProperty |
| One-off screen section | UserControl |
| Computed / read-only control state | DirectProperty + pseudo-class |
| File open/save | TopLevel.StorageProvider |
| Fastest startup, no runtime install | Native AOT publish per RID |
| Installers + auto-update | Velopack (or platform-native packaging) |
| Migrating from 11.x | Read the official "Breaking changes in Avalonia 12" page first: bindings, clipboard, TopLevel, and the removed dialogs all changed |
| Developer tools | AttachDeveloperTools() from AvaloniaUI.DiagnosticsSupport (replaces AttachDevTools) |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

