关于 avalonia
avalonia 是为使用 Avalonia 12 构建桌面应用提供指导的技能,面向 .NET 开发者。内容涵盖编译绑定、跨平台服务、可复用控件,以及修剪和 Native AOT 注意事项。
主要功能
- 01Avalonia 12 编译绑定指导
- 02适配修剪的视图映射
- 03可复用控件与主题
- 04通过 TopLevel 使用跨平台服务
- 05Native AOT 与打包指导
适用场景
- 01构建跨平台 Avalonia 桌面应用
- 02创建可复用且可换肤的控件
- 03为应用准备修剪或 Native AOT
- 04添加文件选择与剪贴板访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill avalonia安装后在 Claude Code 中输入/avalonia确认已加载。
详细介绍
Avalonia UI(v12)
已针对 Avalonia 12.1.x 验证。Avalonia 12 要求 .NET 8+(推荐 .NET 10),并默认启用编译绑定。将此技能与 desktop-mvvm 搭配使用,了解视图模型模式。
核心原则
- 全面使用编译绑定 — 在 Avalonia 12 中,XAML 里的
{Binding}默认会编译(AvaloniaUseCompiledBindingsByDefault=true)。每个视图和DataTemplate都需要x:DataType。绑定拼写错误会变成构建错误,而不是悄无声息地显示空字段;由于不使用反射,裁剪和 AOT 也能正常工作。 - 在编译时将视图映射到视图模型 — 使用类型化的
DataTemplate,或基于字典的ViewLocator。基于反射的模板ViewLocator(Type.GetType+Activator.CreateInstance)在裁剪时会出问题。 - 组合使用
UserControl,复用控件使用TemplatedControl— 无外观的模板化控件可由使用者重新设置样式。UserControl只是屏幕中可复用的一部分。 - 通过
TopLevel使用平台服务 — 使用TopLevel.GetTopLevel(visual)获取文件选择器(StorageProvider)、Clipboard和Launcher。这些服务适用于 Windows、macOS、Linux 和浏览器。不要使用 Win32/WinForms 对话框。 - 在每个目标操作系统上构建和测试 — 字体、DPI、窗口边框和文件路径因平台而异。CI 矩阵(windows/macos/ubuntu)能在用户遇到问题前发现它们。
模式
使用 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();
}
}
原因: OnFrameworkInitializationCompleted 是组合根,此时平台已就绪,但窗口尚未创建。IClassicDesktopStyleApplicationLifetime 检查可确保同一个 App 也能用于移动端和浏览器生命周期。如果显式调用 UseSkia() 后看到“No text shaping system configured”,请添加 Avalonia.HarfBuzz 和 .UseHarfBuzz(),这是 12.0 中的变更。
使用 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>
原因: XAML 编译器会在构建时根据 OrdersViewModel 解析 CustomerName。每个 DataTemplate 都会更改数据上下文,因此需要声明自己的 x:DataType。在 12 中,TextBox.Watermark 已重命名为 PlaceholderText。只有在路径确实是动态的情况下才使用 {ReflectionBinding},并将每次使用都视为裁剪风险。
将视图模型映射到视图(裁剪安全)
<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-mvvm 中的导航)随后会呈现正确的视图。模板的名称约定定位器虽然可以通过编译,但裁剪后会在运行时失效,因为裁剪器会移除“未使用”的视图类型。
StyledProperty 与 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);
}
}
}
原因: 样式属性会经过 Avalonia 的值优先级系统(本地值 > 样式 > 主题 > 继承 > 默认值),会带来少量开销,但支持主题化。直接属性会跳过该系统。:empty 等伪类可让主题在无需额外属性的情况下设置状态样式。
使用 ControlTheme 的模板化控件
<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
}
}
原因: 以类型为键的 ControlTheme 是控件的默认外观,使用者无需派生子类即可替换它。TemplateBinding 是从模板到控件的轻量单向链接。始终要能容忍缺少 PART_,因为使用者可以自由地重新定义模板。
通过 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;
}
}
原因: IStorageProvider 适用于所有平台,包括沙盒化的 macOS 和浏览器,这些环境无法使用原始路径。旧版 OpenFileDialog/SaveFileDialog 类已在 12 中移除。请使用 TopLevel.GetTopLevel(control) 获取 TopLevel,因为在 12 中,将可视化根节点强制转换为 TopLevel 已不可行。
Native AOT 和裁剪
<PropertyGroup>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
检查清单:
- 仅使用编译绑定(默认值为 12);审查所有
{ReflectionBinding}和x:CompileBindings="False" - 确保视图映射可安全裁剪(使用类型化
DataTemplate或字典ViewLocator) - 使用 CommunityToolkit.Mvvm 生成器(不使用反射)和源生成 JSON(
native-aotskill) - 不要在运行时加载 XAML(
AvaloniaRuntimeXamlLoader);仅当你从动态创建的控件所来自的程序集加载控件时,才添加TrimmerRootAssembly
原因: Avalonia 支持 Native AOT,可实现快速冷启动且无需安装运行时,这是相较于 WPF 的一项真正优势。但任何一处反射绑定或定位器都会导致运行时崩溃,而且只有在发布构建中才会显现。务必对 AOT 输出进行冒烟测试。
打包
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-x64
原因: 在对应操作系统上按 RID 发布(Native AOT 需要目标操作系统的原生工具链)。macOS 需要 .app 捆绑包(Info.plist + 图标),并需要签名和公证。在 Windows 上,为 exe 签名。若要在三种操作系统上提供安装程序和自动更新,请使用 Velopack 等专用工具,而不是手动编写更新代码。
反模式
未使用 x:DataType 的绑定
<TextBlock Text="{ReflectionBinding CustmerName}" />
<UserControl x:DataType="vm:OrdersViewModel">
<TextBlock Text="{Binding CustomerName}" />
</UserControl>
在 C# 中使用 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));
在共享 UI 代码中使用平台专属 API
// 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");
在 Avalonia 12 中,每个线程都可以有自己的调度器。只有一个 UI 线程的应用程序代码可以使用 Dispatcher.UIThread,但可复用的控件库应使用控件的 Dispatcher 属性。
决策指南
| 场景 | 建议 |
|---|---|
| 新建跨平台桌面应用 | Avalonia 12 + CommunityToolkit.Mvvm + Generic Host |
| 为视图绑定 | 在根元素和每个 DataTemplate 上设置 x:DataType |
| 确实需要动态绑定路径 | 使用 {ReflectionBinding},并将其隔离在 AOT 构建之外 |
| VM → View 映射 | 在 App.axaml 中使用类型化 DataTemplate(或字典 ViewLocator) |
| 可复用、可重新设置样式的控件 | TemplatedControl + ControlTheme + StyledProperty |
| 一次性屏幕区域 | UserControl |
| 计算得出/只读的控件状态 | DirectProperty + 伪类 |
| 打开/保存文件 | TopLevel.StorageProvider |
| 启动最快且无需安装运行时 | 按 RID 发布 Native AOT |
| 安装程序 + 自动更新 | Velopack(或平台原生打包方案) |
| 从 11.x 迁移 | 先阅读官方的“Avalonia 12 中的重大变更”页面:绑定、剪贴板、TopLevel 和已移除的对话框均有变化 |
| 开发者工具 | 使用 AvaloniaUI.DiagnosticsSupport 中的 AttachDeveloperTools()(替代 AttachDevTools) |