avalonia

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Avalonia UI (v12)

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.
已针对 Avalonia 12.1.x 验证。Avalonia 12 要求 .NET 8+(推荐 .NET 10),并将编译绑定设为默认。将此 skill 与
desktop-mvvm
配合使用,以实现 view-model 模式。

Core Principles

核心原则

  1. Compiled bindings everywhere — In Avalonia 12,
    {Binding}
    in XAML compiles by default (
    AvaloniaUseCompiledBindingsByDefault=true
    ). Every view and
    DataTemplate
    needs
    x:DataType
    . Binding typos become build errors instead of silent blank fields, and there's no reflection, so trimming and AOT keep working.
  2. Views are mapped to view models at compile time — Use typed
    DataTemplate
    s, or a dictionary-based
    ViewLocator
    . The template's reflection
    ViewLocator
    (
    Type.GetType
    +
    Activator.CreateInstance
    ) breaks under trimming.
  3. UserControl
    for composition,
    TemplatedControl
    for reusable controls
    — A lookless templated control can be restyled by consumers. A
    UserControl
    is just a reusable chunk of a screen.
  4. Platform services come from
    TopLevel
    — Use
    TopLevel.GetTopLevel(visual)
    to get the file picker (
    StorageProvider
    ),
    Clipboard
    , and
    Launcher
    . These work on Windows, macOS, Linux, and browser. Never reach for Win32/WinForms dialogs.
  5. 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.
  1. 全面使用编译绑定 — 在 Avalonia 12 中,XAML 中的
    {Binding}
    默认会进行编译(
    AvaloniaUseCompiledBindingsByDefault=true
    )。每个 view 和
    DataTemplate
    都需要
    x:DataType
    。绑定拼写错误会变成构建错误,而不是静默显示空白字段;同时由于不使用反射,裁剪和 AOT 仍可正常工作。
  2. 在编译期将 view 映射到 view model — 使用带类型的
    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. 在每个目标 OS 上构建并测试 — 字体、DPI、窗口框架和文件路径因平台而异。CI 矩阵(windows/macos/ubuntu)可以在问题到达用户之前将其发现。

Patterns

模式

Startup with Generic Host

使用 Generic Host 启动

csharp
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.
csharp
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 的变更。

Compiled Bindings with
x:DataType

使用 x:DataType 实现编译绑定

xml
<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.
xml
<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}
,并将每个此类绑定视为裁剪风险项。

Mapping View Models to Views (Trim-Safe)

View Model 到 View 的映射(裁剪安全)

xml
<!-- 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>
csharp
// 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.
xml
<!-- 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>
csharp
// 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
)随后会渲染正确的 view。按命名约定查找的模板 locator 虽可编译,但在裁剪后会于运行时失败,因为裁剪器会移除“未使用”的 view 类型。

StyledProperty vs DirectProperty

StyledProperty vs DirectProperty

csharp
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.
csharp
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
的伪类可让主题在无需额外属性的情况下设置状态样式。

Templated Control with a ControlTheme

使用 ControlTheme 的模板化控件

xml
<!-- 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>
csharp
// 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.
xml
<!-- 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>
csharp
// 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_
缺失,因为使用者可以自由重新定义模板。

Cross-Platform Services via
TopLevel

通过 TopLevel 使用跨平台服务

csharp
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.
csharp
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 中将 visual root 强制转换为
TopLevel
已不再适用。

Native AOT and Trimming

Native AOT 与裁剪

xml
<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}
    and
    x:CompileBindings="False"
  • Trim-safe view mapping (typed
    DataTemplate
    s or the dictionary
    ViewLocator
    )
  • CommunityToolkit.Mvvm generators (no reflection), source-generated JSON (
    native-aot
    skill)
  • Don't load XAML at runtime (
    AvaloniaRuntimeXamlLoader
    ); add
    TrimmerRootAssembly
    only 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.
xml
<PropertyGroup>
  <PublishAot>true</PublishAot>          <!-- app project only -->
  <IsAotCompatible>true</IsAotCompatible> <!-- every project, enables the analyzers -->
</PropertyGroup>
检查清单:
  • 仅使用编译绑定(12 默认);审查所有
    {ReflectionBinding}
    和
    x:CompileBindings="False"
  • 使用裁剪安全的 view 映射(带类型的
    DataTemplate
    或字典式
    ViewLocator
    )
  • CommunityToolkit.Mvvm 生成器(无反射)、源生成的 JSON(
    native-aot
    skill)
  • 不要在运行时加载 XAML(
    AvaloniaRuntimeXamlLoader
    );仅针对动态创建控件所用的程序集添加
    TrimmerRootAssembly
原因: Avalonia 支持 Native AOT,可实现快速冷启动且无需安装运行时,相比 WPF 具有明显优势。但任何反射绑定或 locator 都可能引发仅在发布构建中出现的运行时崩溃。务必对 AOT 输出执行冒烟测试。

Packaging

打包

bash
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
Why: 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.
bash
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
原因: 在目标 OS 上按 RID 分别发布(Native AOT 需要目标 OS 的原生工具链)。macOS 需要
.app
bundle(Info.plist + icon),并进行签名和 notarization。在 Windows 上,需要为 exe 签名。若需覆盖三个 OS 的安装程序和自动更新,应使用 Velopack 等专用工具,而不要自行编写更新代码。

Anti-patterns

反模式

Bindings Without
x:DataType

未使用 x:DataType 的绑定

xml
<!-- 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>
xml
<!-- 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

在 C# 中使用 Binding 类进行绑定

csharp
// 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));
csharp
// 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

共享 UI 代码中的平台特定 API

csharp
// 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));
csharp
// 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

从后台线程操作控件

csharp
// 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.
csharp
// 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 中,每个线程都可以拥有自己的 dispatcher。只有单一 UI 线程的 App 代码可以使用
Dispatcher.UIThread
,但可复用控件库应使用控件自身的
Dispatcher
属性。

Decision Guide

决策指南

ScenarioRecommendation
New cross-platform desktop appAvalonia 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 mappingTyped
DataTemplate
s 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 installNative AOT publish per RID
Installers + auto-updateVelopack (or platform-native packaging)
Migrating from 11.xRead 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
)
场景建议
新的跨平台桌面应用Avalonia 12 + CommunityToolkit.Mvvm + Generic Host
绑定 view在根元素和每个
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
)