mcpField
A
avalonia

avalonia

github.com/codewithmukesh开发807153 次安装2 天前更新
代码仓库

关于 avalonia

avalonia 是为使用 Avalonia 12 构建桌面应用提供指导的技能,面向 .NET 开发者。内容涵盖编译绑定、跨平台服务、可复用控件,以及修剪和 Native AOT 注意事项。

主要功能

  1. 01Avalonia 12 编译绑定指导
  2. 02适配修剪的视图映射
  3. 03可复用控件与主题
  4. 04通过 TopLevel 使用跨平台服务
  5. 05Native AOT 与打包指导

适用场景

  1. 01构建跨平台 Avalonia 桌面应用
  2. 02创建可复用且可换肤的控件
  3. 03为应用准备修剪或 Native AOT
  4. 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 搭配使用,了解视图模型模式。

核心原则

  1. 全面使用编译绑定 — 在 Avalonia 12 中,XAML 里的 {Binding} 默认会编译(AvaloniaUseCompiledBindingsByDefault=true)。每个视图和 DataTemplate 都需要 x:DataType。绑定拼写错误会变成构建错误,而不是悄无声息地显示空字段;由于不使用反射,裁剪和 AOT 也能正常工作。
  2. 在编译时将视图映射到视图模型 — 使用类型化的 DataTemplate,或基于字典的 ViewLocator。基于反射的模板 ViewLocator(Type.GetType + Activator.CreateInstance)在裁剪时会出问题。
  3. 组合使用 UserControl,复用控件使用 TemplatedControl — 无外观的模板化控件可由使用者重新设置样式。UserControl 只是屏幕中可复用的一部分。
  4. 通过 TopLevel 使用平台服务 — 使用 TopLevel.GetTopLevel(visual) 获取文件选择器(StorageProvider)、Clipboard 和 Launcher。这些服务适用于 Windows、macOS、Linux 和浏览器。不要使用 Win32/WinForms 对话框。
  5. 在每个目标操作系统上构建和测试 — 字体、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-aot skill)
  • 不要在运行时加载 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)

评论

支持 Markdown