avalonia
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAvalonia 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 for view-model patterns.
desktop-mvvm已针对 Avalonia 12.1.x 验证。Avalonia 12 要求 .NET 8+(推荐 .NET 10),并将编译绑定设为默认。将此 skill 与 配合使用,以实现 view-model 模式。
desktop-mvvmCore Principles
核心原则
- Compiled bindings everywhere — In Avalonia 12, in XAML compiles by default (
{Binding}). Every view andAvaloniaUseCompiledBindingsByDefault=trueneedsDataTemplate. Binding typos become build errors instead of silent blank fields, and there's no reflection, so trimming and AOT keep working.x:DataType - Views are mapped to view models at compile time — Use typed s, or a dictionary-based
DataTemplate. The template's reflectionViewLocator(ViewLocator+Type.GetType) breaks under trimming.Activator.CreateInstance - for composition,
UserControlfor reusable controls — A lookless templated control can be restyled by consumers. ATemplatedControlis just a reusable chunk of a screen.UserControl - Platform services come from — Use
TopLevelto get the file picker (TopLevel.GetTopLevel(visual)),StorageProvider, andClipboard. These work on Windows, macOS, Linux, and browser. Never reach for Win32/WinForms dialogs.Launcher - 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.
- 全面使用编译绑定 — 在 Avalonia 12 中,XAML 中的 默认会进行编译(
{Binding})。每个 view 和AvaloniaUseCompiledBindingsByDefault=true都需要DataTemplate。绑定拼写错误会变成构建错误,而不是静默显示空白字段;同时由于不使用反射,裁剪和 AOT 仍可正常工作。x:DataType - 在编译期将 view 映射到 view model — 使用带类型的 ,或基于字典的
DataTemplate。基于反射的模板ViewLocator(ViewLocator+Type.GetType)在裁剪后无法正常工作。Activator.CreateInstance - 组合使用 ,可复用控件使用
UserControl— 无外观的模板化控件可由使用者重新设置样式。TemplatedControl只是屏幕中可重复使用的一部分。UserControl - 通过 获取平台服务 — 使用
TopLevel获取文件选择器(TopLevel.GetTopLevel(visual))、StorageProvider和Clipboard。这些功能适用于 Windows、macOS、Linux 和浏览器。切勿使用 Win32/WinForms 对话框。Launcher - 在每个目标 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: is the composition root, the point where the platform is ready but no window exists yet. The check keeps the same usable on mobile and browser lifetimes. If you call explicitly and see "No text shaping system configured", add and , a 12.0 change.
OnFrameworkInitializationCompletedIClassicDesktopStyleApplicationLifetimeAppUseSkia()Avalonia.HarfBuzz.UseHarfBuzz()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();
}
}原因: 是组合根,此时平台已准备就绪,但尚未创建窗口。检查 可让同一个 继续用于移动端和浏览器的生命周期。如果显式调用 后出现“No text shaping system configured”错误,请添加 和 ,这是 12.0 的变更。
OnFrameworkInitializationCompletedIClassicDesktopStyleApplicationLifetimeAppUseSkia()Avalonia.HarfBuzz.UseHarfBuzz()Compiled Bindings with x:DataType
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 against at build time. Each changes the data context, so it declares its own . In 12, was renamed . Use only for a genuinely dynamic path, and treat each one as a trimming liability.
CustomerNameOrdersViewModelDataTemplatex:DataTypeTextBox.WatermarkPlaceholderText{ReflectionBinding}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 编译器会在构建时根据 解析 。每个 都会改变数据上下文,因此需要声明自己的 。在 12 中, 已更名为 。仅当绑定路径确实动态时才使用 ,并将每个此类绑定视为裁剪风险项。
OrdersViewModelCustomerNameDataTemplatex:DataTypeTextBox.WatermarkPlaceholderText{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: (see navigation in ) 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.
<ContentControl Content="{Binding Current}" />desktop-mvvmxml
<!-- 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;
}原因: (有关导航,请参阅 )随后会渲染正确的 view。按命名约定查找的模板 locator 虽可编译,但在裁剪后会于运行时失败,因为裁剪器会移除“未使用”的 view 类型。
<ContentControl Content="{Binding Current}" />desktop-mvvmStyledProperty 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 let themes style a state without extra properties.
:emptycsharp
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 的值优先级系统(本地值 > 样式 > 主题 > 继承 > 默认值)处理,因此会略有开销,但支持主题化。直接属性会跳过该系统。诸如 的伪类可让主题在无需额外属性的情况下设置状态样式。
:emptyTemplated 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 keyed by type is the control's default look, and consumers replace it without subclassing. is the cheap one-way link from template to control. Always tolerate a missing because consumers are free to re-template.
ControlThemeTemplateBindingPART_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
}
}原因: 以类型为键的 是控件的默认外观,使用者无需创建子类即可替换它。 是从模板到控件的低成本单向链接。必须始终容忍 缺失,因为使用者可以自由重新定义模板。
ControlThemeTemplateBindingPART_Cross-Platform Services via TopLevel
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: works on every platform, including sandboxed macOS and the browser, where raw paths aren't available. The old / classes were removed in 12. Get the with , since casting the visual root to no longer works in 12.
IStorageProviderOpenFileDialogSaveFileDialogTopLevelTopLevel.GetTopLevel(control)TopLevelcsharp
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;
}
}原因: 可在所有平台上工作,包括沙箱化的 macOS 和浏览器,在这些环境中无法使用原始路径。旧的 / 类在 12 中已被移除。请使用 获取 ,因为在 12 中将 visual root 强制转换为 已不再适用。
IStorageProviderOpenFileDialogSaveFileDialogTopLevel.GetTopLevel(control)TopLevelTopLevelNative 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 and
{ReflectionBinding}x:CompileBindings="False" - Trim-safe view mapping (typed s or the dictionary
DataTemplate)ViewLocator - CommunityToolkit.Mvvm generators (no reflection), source-generated JSON (skill)
native-aot - Don't load XAML at runtime (); add
AvaloniaRuntimeXamlLoaderonly for assemblies you create controls from dynamicallyTrimmerRootAssembly
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(skill)
native-aot - 不要在运行时加载 XAML();仅针对动态创建控件所用的程序集添加
AvaloniaRuntimeXamlLoaderTrimmerRootAssembly
原因: 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-x64Why: publish per RID on that OS (Native AOT needs the native toolchain of the target OS). macOS needs an 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.
.appbash
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 需要 bundle(Info.plist + icon),并进行签名和 notarization。在 Windows 上,需要为 exe 签名。若需覆盖三个 OS 的安装程序和自动更新,应使用 Velopack 等专用工具,而不要自行编写更新代码。
.appAnti-patterns
反模式
Bindings Without x:DataType
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
Binding在 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 , but reusable control libraries should use the control's property.
Dispatcher.UIThreadDispatchercsharp
// 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.UIThreadDispatcherDecision Guide
决策指南
| 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 | |
| 场景 | 建议 |
|---|---|
| 新的跨平台桌面应用 | Avalonia 12 + CommunityToolkit.Mvvm + Generic Host |
| 绑定 view | 在根元素和每个 |
| 真正动态的绑定路径 | |
| VM → View 映射 | |
| 可复用且可重新设置样式的控件 | |
| 一次性屏幕区块 | |
| 计算得出的只读控件状态 | |
| 打开/保存文件 | |
| 启动最快且无需安装运行时 | 按 RID 分别执行 Native AOT 发布 |
| 安装程序 + 自动更新 | Velopack(或平台原生打包方式) |
| 从 11.x 迁移 | 先阅读官方“Avalonia 12 中的破坏性变更”页面:绑定、剪贴板、 |
| 开发者工具 | 来自 |