Loading...
Loading...
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".
npx skill4agent add codewithmukesh/dotnet-claude-kit avaloniadesktop-mvvm{Binding}AvaloniaUseCompiledBindingsByDefault=trueDataTemplatex:DataTypeDataTemplateViewLocatorViewLocatorType.GetTypeActivator.CreateInstanceUserControlTemplatedControlUserControlTopLevelTopLevel.GetTopLevel(visual)StorageProviderClipboardLauncherinternal 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();
}
}OnFrameworkInitializationCompletedIClassicDesktopStyleApplicationLifetimeAppUseSkia()Avalonia.HarfBuzz.UseHarfBuzz()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>CustomerNameOrdersViewModelDataTemplatex:DataTypeTextBox.WatermarkPlaceholderText{ReflectionBinding}<!-- 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;
}<ContentControl Content="{Binding Current}" />desktop-mvvmpublic 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);
}
}
}:empty<!-- 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
}
}ControlThemeTemplateBindingPART_TopLevelpublic 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;
}
}IStorageProviderOpenFileDialogSaveFileDialogTopLevelTopLevel.GetTopLevel(control)TopLevel<PropertyGroup>
<PublishAot>true</PublishAot> <!-- app project only -->
<IsAotCompatible>true</IsAotCompatible> <!-- every project, enables the analyzers -->
</PropertyGroup>{ReflectionBinding}x:CompileBindings="False"DataTemplateViewLocatornative-aotAvaloniaRuntimeXamlLoaderTrimmerRootAssemblydotnet 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-x64.appx: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>Binding// 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));// 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));// 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");Dispatcher.UIThreadDispatcher| Scenario | Recommendation |
|---|---|
| New cross-platform desktop app | Avalonia 12 + CommunityToolkit.Mvvm + Generic Host |
| Binding a view | |
| Truly dynamic binding path | |
| VM → View mapping | Typed |
| Reusable, restylable control | |
| One-off screen section | |
| Computed / read-only control state | |
| File open/save | |
| 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, |
| Developer tools | |