关于 avalonia-api
面向使用 .NET 构建 Avalonia UI 的开发者,提供 AXAML 实用参考,涵盖控件、布局、绑定、MVVM 和样式,并包含 XerahS 专用建议。
主要功能
- 01AXAML 控件与布局指南
- 02编译绑定和 MVVM 模式
- 03样式、主题与资源
- 04自定义属性和附加属性
- 05响应式与可滚动布局实践
适用场景
- 01开发视图时查找 Avalonia UI 模式
- 02为响应式界面选择布局
- 03应用绑定和 MVVM 约定
- 04排查滚动和上下文菜单问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sharex/ xerahs --skill avalonia-api安装后在 Claude Code 中输入/avalonia-api确认已加载。
详细介绍
Avalonia UI 框架——完整 API 与最佳实践指南
目标框架:.NET 10.0+
文件扩展名:.axaml(Avalonia XAML)
官方文档:https://docs.avaloniaui.net/
目录
- AXAML 基础知识
- 控件与 UI 元素
- 布局系统
- 数据绑定
- 使用 CommunityToolkit.Mvvm 的 MVVM 模式
- 样式与主题
- 依赖属性与附加属性
- 自定义控件
- 控件模板
- 资源与转换器
- 事件与命令
- 导航
- 跨平台模式
- 性能与最佳实践
- 开发者工具
- 应避免的常见错误
- XerahS 中的常见模式
AXAML 基础知识
文件结构
每个 .axaml 文件都遵循以下标准结构:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:YourApp.ViewModels"
x:Class="YourApp.Views.MainWindow"
x:DataType="vm:MainViewModel"
x:CompileBindings="True">
</Window>
必需的命名空间声明
| 命名空间 | 用途 | 必需 |
|---|---|---|
xmlns="https://github.com/avaloniaui" | Avalonia 核心控件 | ✅ 始终需要 |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | XAML 语言功能 | ✅ 始终需要 |
xmlns:vm="using:YourNamespace.ViewModels" | ViewModel 引用 | ⚠️ 用于 MVVM |
xmlns:local="using:YourNamespace" | 本地类型/控件 | 🔹 按需使用 |
自定义命名空间语法
<xmlns:myAlias1="using:AppNameSpace.MyNamespace">
<xmlns:myAlias2="clr-namespace:OtherAssembly.MyNameSpace;assembly=OtherAssembly">
<xmlns:controls="using:XerahS.UI.Controls">
控件内容与属性
<Button>Hello World!</Button>
<Button Content="Hello World!" />
<Button>
<Button.Content>
<StackPanel>
<TextBlock Text="Complex" />
<TextBlock Text="Content" />
</StackPanel>
</Button.Content>
</Button>
控件与 UI 元素
常见内置控件
输入控件
- TextBox:单行/多行文本输入
- PasswordBox:密码掩码输入
- NumericUpDown:可递增/递减的数字输入
- CheckBox:布尔值切换
- RadioButton:互斥选择
- Slider:连续范围选择
- ComboBox:下拉选择
- AutoCompleteBox:带建议的文本输入
- DatePicker:日期选择
- TimePicker:时间选择
- ColorPicker:颜色选择
显示控件
- TextBlock:只读文本显示
- Label:支持访问键的文本
- Image:显示图像
- Border:内容周围的可视边框
- ContentControl:单内容容器
布局面板
- Panel:基本容器(填充可用空间)
- StackPanel:垂直/水平堆叠
- Grid:行/列网格布局
- DockPanel:边缘停靠布局
- Canvas:绝对定位
- WrapPanel:自动换行流式布局
- RelativePanel:相对定位
- UniformGrid:大小相同的单元格
列表与集合
- ListBox:可选择列表
- ListView:支持自定义视图的列表
- TreeView:分层树
- DataGrid:带列的表格数据
- ItemsControl:集合显示基类
- ItemsRepeater:虚拟化集合
容器
- Window:顶级窗口
- UserControl:可复用 UI 组件
- ScrollViewer:可滚动内容
- Expander:可折叠内容
- TabControl:选项卡界面
- SplitView:汉堡菜单模式
按钮
- Button:标准按钮
- ToggleButton:双状态按钮
- RepeatButton:自动重复按钮
- RadioButton:互斥按钮
- SplitButton:带下拉菜单的按钮
- DropDownButton:下拉菜单按钮
高级
- Carousel:循环播放内容
- MenuFlyout:基于现代浮出菜单的上下文菜单(⚠️ 与 FluentAvalonia 一起使用)
- ContextFlyout:右键菜单容器(⚠️ 优先于 ContextMenu 使用)
- ContextMenu:旧版右键菜单(⚠️ 避免与 FluentAvalonia 主题一起使用)
- Menu:菜单栏
- ToolTip:悬停时显示的信息
- Flyout:弹出式浮层
- Calendar:日历显示
布局系统
ScrollViewer 激活要求
只有当 ScrollViewer 在 Measure 阶段从父级接收到有限(有界)的高度约束时,才会激活(显示并启用滚动条)。如果任一祖先沿布局链向下传递 ∞(无穷大),ScrollViewer 就永远无法滚动。
XerahS 布局中常见的无限高度来源及其修复方法:
| 根本原因 | 滚动失效的原因 | 修复方法 |
|---|---|---|
SplitView 作为双列外壳 | 继承自 ContentControl;默认 VerticalContentAlignment=Top → ContentPresenter 会传递 ∞ 高度 | 替换为 Grid ColumnDefinitions="auto,*" |
TransitioningContentControl 作为页面承载控件 | 内部动画 Panel 在测量期间会传递 ∞ 高度 | 替换为 ContentControl HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" |
TabControl 未设置 VerticalContentAlignment="Stretch" | 内部 ContentPresenter 模板绑定到 {TemplateBinding VerticalContentAlignment};默认值为 Top → 向选项卡内容传递 ∞ | 为 TabControl 添加 VerticalContentAlignment="Stretch" |
ScrollViewer Padding="N" | 会缩小视口,但不会将 N 加入滚动范围——内容底部的 Npx 永远无法访问 | 从 ScrollViewer 移除 Padding;改为在内部的 StackPanel/Grid 上添加 Margin="N" |
可滚动设置页面的标准模式:
<TabControl VerticalContentAlignment="Stretch">
<TabItem Header="General">
<ScrollViewer>
<StackPanel Spacing="24" Margin="24">
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
<TabControl>
<TabItem Header="General">
<ScrollViewer Padding="24">
<StackPanel Spacing="24">
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
布局流程
Avalonia 使用两阶段布局系统:
- 测量阶段:确定每个控件的期望大小
- 排列阶段:在可用空间内放置控件
Control → Measure → MeasureOverride → DesiredSize
→ Arrange → ArrangeOverride → FinalSize
面板对比
| 面板 | 使用场景 | 性能 | 复杂度 |
|---|---|---|---|
| Panel | 填充可用空间 | ⚡ 最佳 | 简单 |
| StackPanel | 线性堆叠 | ⚡ 良好 | 简单 |
| Canvas | 绝对定位 | ⚡ 良好 | 简单 |
| DockPanel | 边缘停靠 | ✅ 良好 | 中等 |
| Grid | 复杂布局 | ⚠️ 一般 | 复杂 |
| RelativePanel | 相对约束 | ⚠️ 一般 | 复杂 |
建议:若不需要行列,请使用 Panel 代替 Grid,以获得更好的性能。
常见布局属性
<Control Width="100"
Height="50"
MinWidth="50"
MaxWidth="200"
Margin="10,5,10,5"
Padding="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
Grid 布局
<Grid RowDefinitions="Auto,*,50"
ColumnDefinitions="200,*,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Header" />
<Border Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" />
<Grid ColumnDefinitions="*,2*,*">
</Grid>
</Grid>
DockPanel 布局
<DockPanel LastChildFill="True">
<Menu DockPanel.Dock="Top" />
<StatusBar DockPanel.Dock="Bottom" />
<TreeView DockPanel.Dock="Left" Width="200" />
<ContentControl Content="{Binding CurrentView}" />
</DockPanel>
StackPanel 布局
<StackPanel Orientation="Vertical"
Spacing="10">
<TextBlock Text="Item 1" />
<TextBlock Text="Item 2" />
<TextBlock Text="Item 3" />
</StackPanel>
GridSplitter(可调整大小的窗格)
当 Grid 包含不同的内容区域(侧边栏 + 主区域、上下分区)时,添加 GridSplitter,以便用户调整窗格大小。为分隔条预留一个较窄的列或行(3–6 px)。
<Grid ColumnDefinitions="250, 4, *">
<TreeView Grid.Column="0" />
<GridSplitter Grid.Column="1" ResizeDirection="Columns" />
<ContentControl Grid.Column="2" Content="{Binding Detail}" />
</Grid>
关键规则:
ResizeDirection必须与轴向匹配:列分隔条使用Columns,行分隔条使用Rows。- 在相邻单元格上设置
MinWidth/MaxWidth(或MinHeight/MaxHeight),防止其缩小到零。 - 只要 Grid 中有两个或更多可调整大小的内容区域,就使用
GridSplitter。
响应式布局
Avalonia 提供了四种实现方式。优先使用这些方式,而不是固定像素布局。
容器查询(可复用组件首选)
根据祖先控件的大小做出响应,而不是根据窗口大小。控件调整大小时会实时生效。
<Border Container.Name="main" Container.Sizing="Width">
<Panel.Styles>
<ContainerQuery Name="main" Query="max-width:600">
<Style Selector="StackPanel#sidebar">
<Setter Property="IsVisible" Value="False" />
</Style>
</ContainerQuery>
</Panel.Styles>
</Border>
- 组合条件:
Query="min-width:400 and max-width:800" Container.Sizing:Width、Height或Width Height
OnFormFactor(静态平台检测)
在启动时解析一次。用于桌面与移动设备之间不会随窗口大小变化而改变的差异。
<Grid ColumnDefinitions="{OnFormFactor Desktop='250,*', Mobile='*'}">
<Border IsVisible="{OnFormFactor Desktop=True, Mobile=False}" />
</Grid>
自适应重排面板(自我适应)
<ItemsRepeater ItemsSource="{Binding Items}">
<ItemsRepeater.Layout>
<UniformGridLayout MinItemWidth="200" MinItemHeight="150" />
</ItemsRepeater.Layout>
</ItemsRepeater>
何时使用哪种方式
| 场景 | 方式 |
|---|---|
| 可复用组件根据自身大小进行适配 | 容器查询 |
| 桌面与移动端布局(静态) | OnFormFactor |
| 可换行排列的卡片/图块 | WrapPanel 或 UniformGridLayout |
| 在断点处进行复杂的多属性更改 | 断点 ViewModel(观察窗口大小,公开布尔属性) |
关键规则:
- 优先使用容器查询,而不是手动处理大小变化事件。
- Grid 定义中始终使用星号尺寸(
*)和Auto——避免为内容区域指定固定像素宽度。 Visibility枚举已由bool IsVisible取代;如需隐藏但保留占用空间,请使用Opacity="0"。- 不要使用
VisualStateManager——改用伪类选择器或容器查询。
数据绑定
绑定语法
<TextBlock Text="{Binding PropertyName}" />
<TextBlock Text="{Binding Person.Name}" />
<TextBox Text="{Binding Name, Mode=TwoWay}" />
<TextBlock x:Name="MyText" Text="Hello" />
<TextBox Text="{Binding #MyText.Text}" />
<TextBlock Text="{Binding $parent[Window].DataContext.Title}" />
<TextBlock Text="{Binding Name, FallbackValue='Unknown'}" />
<TextBlock Text="{Binding Price, StringFormat='${0:F2}'}" />
<TextBlock Text="{Binding IsEnabled, Converter={StaticResource BoolToStringConverter}}" />
重要提示:
#ElementName.Property是 Avalonia 绑定路径扩展,应与 AvaloniaBinding/ 编译绑定搭配使用。- 不要写
{ReflectionBinding #SomeElement.SomeCommand}。ReflectionBinding会将#...标记视为普通路径段,因此运行时可能无法查找命令/属性。
编译绑定(推荐)
编译绑定提供编译时安全性和更好的性能。
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Window x:DataType="vm:MainViewModel"
x:CompileBindings="True">
<TextBox Text="{Binding FirstName}" />
<TextBox Text="{Binding LastName}" />
<Button Command="{ReflectionBinding DynamicCommandName}" />
</Window>
<TextBox Text="{CompiledBinding FirstName}" />
最佳实践:始终使用编译绑定以确保类型安全并提升性能。仅当路径确实是动态的、无法进行类型化时,才使用 ReflectionBinding;绝不要与 #ElementName 语法搭配使用。
DataContext 类型推断(v11.3+)
<Window x:Name="MyWindow"
x:DataType="vm:TestDataContext">
<TextBlock Text="{Binding #MyWindow.DataContext.StringProperty}" />
<TextBlock Text="{Binding $parent[Window].DataContext.StringProperty}" />
</Window>
多重绑定
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
元素绑定
<Slider x:Name="volumeSlider" Minimum="0" Maximum="100" Value="50" />
<TextBlock Text="{Binding #volumeSlider.Value}" />
<Border BorderThickness="{Binding $parent.IsMouseOver,
Converter={StaticResource BoolToThicknessConverter}}" />
使用 CommunityToolkit.Mvvm 的 MVVM 模式
⚠️ XerahS 使用
CommunityToolkit.Mvvm,而非 ReactiveUI。 不要添加或引用ReactiveUI或Avalonia.ReactiveUI包。
安装包
dotnet add package CommunityToolkit.Mvvm
ViewModel 基础模式
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _firstName = string.Empty;
[ObservableProperty]
private bool _isLoading;
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
IsLoading = true;
try
{
await Task.Delay(1000); // Simulate save
}
finally
{
IsLoading = false;
}
}
private bool CanSave() => !string.IsNullOrWhiteSpace(FirstName);
}
源代码生成器说明:
- 在
private字段上使用[ObservableProperty]会生成一个公开的 PascalCase 属性,并发出INotifyPropertyChanged通知(_firstName→FirstName)。 [RelayCommand]会自动生成SaveAsyncCommand(一个IAsyncRelayCommand)。- 为使源代码生成器正常工作,类必须为
partial。 [RelayCommand(CanExecute = nameof(...))]会自动绑定 CanExecute;条件发生变化时,调用SaveAsyncCommand.NotifyCanExecuteChanged()。
架构分层
- 视图(AXAML):仅负责视觉组合。除
InitializeComponent()外,代码隐藏中不包含业务逻辑。 - ViewModel:负责状态、命令和编排。与 UI 框架无关,且可进行单元测试。通过 DI 接入服务。
- 服务 / 领域层:业务逻辑和数据访问——不引用 Avalonia 类型。
视图设置(代码隐藏)
public partial class MainView : UserControl
{
public MainView()
{
InitializeComponent();
DataContext = new MainViewModel(); // Or resolve via ViewLocator / DI
}
}
XAML 中的命令绑定
<Button Content="Save" Command="{Binding SaveAsyncCommand}" />
<Button Content="Delete"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedItem}" />
属性更改回调
[ObservableProperty]
private string _name = string.Empty;
// Source-generated partial method — called automatically when Name changes
partial void OnNameChanged(string value)
{
// React to change
}
手动属性通知(源代码生成器不可用时)
public partial class MyViewModel : ObservableObject
{
private string _title = string.Empty;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
}
样式与主题
样式类型
Avalonia 有三种样式机制:
- 样式:类似 CSS,按类型或类定位控件
- 控件主题:完整的视觉模板(类似 WPF 样式)
- 容器查询:根据容器大小应用响应式样式
基本样式
<Window.Styles>
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
</Style>
<Style Selector="TextBlock.header">
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="LightBlue" />
</Style>
<Style Selector="StackPanel > Button">
<Setter Property="Margin" Value="5" />
</Style>
</Window.Styles>
<TextBlock Classes="header" Text="Title" />
伪类
<Style Selector="Button:pointerover">
<Style Selector="Button:pressed">
<Style Selector="Button:disabled">
<Style Selector="ListBoxItem:selected">
<Style Selector="TextBox:focus">
<Style Selector="CheckBox:checked">
<Style Selector="ToggleButton:unchecked">
样式选择器
<Style Selector="StackPanel TextBlock">
<Style Selector="StackPanel > TextBlock">
<Style Selector="Button.primary:pointerover">
<Style Selector="Button, ToggleButton">
<Style Selector="Button:not(.primary)">
<Style Selector="Button /template/ ContentPresenter">
资源
<Window.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#007ACC" />
<x:Double x:Key="StandardSpacing">10</x:Double>
<LinearGradientBrush x:Key="GradientBrush" StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Color="#FF0000" Offset="0" />
<GradientStop Color="#00FF00" Offset="1" />
</LinearGradientBrush>
</Window.Resources>
<Button Background="{StaticResource PrimaryBrush}"
Margin="{StaticResource StandardSpacing}" />
<Button Background="{DynamicResource PrimaryBrush}" />
主题
<Application.Styles>
<FluentTheme />
<SimpleTheme />
<StyleInclude Source="/Styles/CustomStyles.axaml" />
</Application.Styles>
依赖属性与附加属性
StyledProperty(依赖属性)
public class MyControl : ContentControl
{
// Define the property
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<MyControl, string>(
nameof(Title),
defaultValue: string.Empty);
// CLR wrapper
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
// React to property changes
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == TitleProperty)
{
// Handle change
var oldValue = (string?)change.OldValue;
var newValue = (string?)change.NewValue;
}
}
}
附加属性
public class MyPanel : Panel
{
// Define attached property
public static readonly AttachedProperty<int> ColumnProperty =
AvaloniaProperty.RegisterAttached<MyPanel, Control, int>(
"Column",
defaultValue: 0);
// Getters/Setters
public static int GetColumn(Control element)
=> element.GetValue(ColumnProperty);
public static void SetColumn(Control element, int value)
=> element.SetValue(ColumnProperty, value);
}
<local:MyPanel>
<Button local:MyPanel.Column="0" Content="First" />
<Button local:MyPanel.Column="1" Content="Second" />
</local:MyPanel>
常见附加属性
<Button Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3" />
<Menu DockPanel.Dock="Top" />
<Rectangle Canvas.Left="50" Canvas.Top="100" />
<Button ToolTip.Tip="Click me!" />
<Border>
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Copy" />
<MenuItem Header="Paste" />
</MenuFlyout>
</Border.ContextFlyout>
</Border>
⚠️ XerahS 专属:ContextMenu 与 ContextFlyout
FluentAvalonia 主题的关键问题
问题:标准 ContextMenu 控件无法与 FluentAvaloniaTheme 正常呈现。它们使用旧版 Popup 窗口,样式不完整,可能显示为无样式或不可见。
解决方案:✅ 始终使用 ContextFlyout 与 MenuFlyout,而不是 ContextMenu。
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Action" Command="{Binding MyCommand}"/>
</ContextMenu>
</Border.ContextMenu>
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Action" Command="{Binding MyCommand}"/>
</MenuFlyout>
</Border.ContextFlyout>
DataTemplate 中使用 Flyout 绑定
问题:在 DataTemplate 中使用 ContextFlyout 或 ContextMenu 时,绑定到父级 ViewModel 会失败,因为 Popup/Flyout 位于独立的可视化树中,与 DataTemplate 的层级分离。
解决方案:使用 $parent[UserControl].DataContext 访问主视图的 DataContext。
<DataTemplate x:DataType="local:MyItem">
<Border>
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Edit"
Command="{Binding $parent[UserControl].DataContext.EditCommand}"
CommandParameter="{Binding}"/>
</MenuFlyout>
</Border.ContextFlyout>
<TextBlock Text="{Binding Name}" />
</Border>
</DataTemplate>
要点:
- 使用
$parent[UserControl].DataContext在 flyout 中访问视图的 ViewModel CommandParameter="{Binding}"会传递当前数据项(DataTemplate 的 DataContext)- 对于共享 flyout,在
UserControl.Resources中定义,并通过{StaticResource}引用
自定义控件
自绘制的自定义控件
public class CircleControl : Control
{
public static readonly StyledProperty<IBrush?> FillProperty =
AvaloniaProperty.Register<CircleControl, IBrush?>(nameof(Fill));
public IBrush? Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public override void Render(DrawingContext context)
{
var renderSize = Bounds.Size;
var center = new Point(renderSize.Width / 2, renderSize.Height / 2);
var radius = Math.Min(renderSize.Width, renderSize.Height) / 2;
context.DrawEllipse(Fill, null, center, radius, radius);
}
}
模板化控件(无外观)
public class MyButton : TemplatedControl
{
public static readonly StyledProperty<string> TextProperty =
AvaloniaProperty.Register<MyButton, string>(nameof(Text));
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
// Find template parts
var presenter = e.NameScope.Find<ContentPresenter>("PART_ContentPresenter");
}
}
UserControl(组合控件)
<UserControl xmlns="https://github.com/avaloniaui"
x:Class="MyApp.Controls.MyUserControl">
<StackPanel>
<TextBlock Text="{Binding Title}" />
<Button Content="Click Me" />
</StackPanel>
</UserControl>
// MyUserControl.axaml.cs
public partial class MyUserControl : UserControl
{
public MyUserControl()
{
InitializeComponent();
}
}
控件模板
定义 ControlTheme
<ControlTheme x:Key="CustomButtonTheme" TargetType="Button">
<Setter Property="Background" Value="Blue" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="10,5" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="5">
<ContentPresenter Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border">
<Setter Property="Background" Value="LightBlue" />
</Style>
<Style Selector="^:pressed /template/ Border">
<Setter Property="Background" Value="DarkBlue" />
</Style>
</ControlTheme>
<Button Theme="{StaticResource CustomButtonTheme}" Content="Custom" />
模板部件
[TemplatePart("PART_ContentPresenter", typeof(ContentPresenter))]
public class MyTemplatedControl : TemplatedControl
{
private ContentPresenter? _presenter;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_presenter = e.NameScope.Find<ContentPresenter>("PART_ContentPresenter");
}
}
资源与转换器
值转换器
public class BoolToVisibilityConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool boolValue)
return boolValue ? true : false; // Or specific logic
return false;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool visible)
return visible;
return false;
}
}
<Window.Resources>
<local:BoolToVisibilityConverter x:Key="BoolToVisConverter" />
</Window.Resources>
<Border IsVisible="{Binding IsActive, Converter={StaticResource BoolToVisConverter}}" />
内置转换器
<Button IsEnabled="{Binding !IsLoading}" />
<TextBlock IsVisible="{Binding MyObject, Converter={x:Static ObjectConverters.IsNotNull}}" />
<TextBlock Text="{Binding Count, StringFormat='Items: {0}'}" />
事件与命令
事件处理程序
<Button Click="OnButtonClick" Content="Click" />
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
// Handle event
}
命令(MVVM)
<Button Command="{Binding SaveCommand}"
CommandParameter="{Binding CurrentItem}"
Content="Save" />
// Using CommunityToolkit.Mvvm
[RelayCommand]
private void Save(object? parameter)
{
// Execute command
}
// Async command
[RelayCommand]
private async Task SaveAsync()
{
await Task.Delay(100);
}
// Command with can-execute
[RelayCommand(CanExecute = nameof(CanDelete))]
private void Delete() { }
private bool CanDelete() => SelectedItem is not null;
路由事件
public static readonly RoutedEvent<RoutedEventArgs> MyEvent =
RoutedEvent.Register<MyControl, RoutedEventArgs>(
nameof(MyEvent),
RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> MyEvent
{
add => AddHandler(MyEvent, value);
remove => RemoveHandler(MyEvent, value);
}
// Raise event
RaiseEvent(new RoutedEventArgs(MyEvent));
跨平台模式
平台检测
if (OperatingSystem.IsWindows())
{
// Windows-specific code
}
else if (OperatingSystem.IsMacOS())
{
// macOS-specific code
}
else if (OperatingSystem.IsLinux())
{
// Linux-specific code
}
特定于平台的资源
<Application.Styles>
<StyleInclude Source="/Styles/Common.axaml" />
<OnPlatform>
<On Options="Windows">
<StyleInclude Source="/Styles/Windows.axaml" />
</On>
<On Options="macOS">
<StyleInclude Source="/Styles/macOS.axaml" />
</On>
</OnPlatform>
</Application.Styles>
设计原则
- 使用 .NET Standard:在 .NET Standard 库中编写业务逻辑
- MVVM 模式:将 UI 与逻辑分离
- Avalonia 绘制:利用 Avalonia 绘制的 UI(而非原生控件)
- 平台抽象:使用接口实现特定于平台的功能
- 响应式设计:使用容器查询和自适应布局
性能与最佳实践
性能提示
- 无需行或列时使用
Panel而非Grid - 全局启用编译绑定
- 为大型列表使用虚拟化(
ItemsRepeater、VirtualizingStackPanel) - 避免可视化树嵌套过深
- 动画中使用
RenderTransform而非Margin - 使用
ItemsRepeater回收 DataTemplate - 通过批量更改属性来尽量减少布局遍历次数
内存管理
CommunityToolkit.Mvvm 源生成的属性会自动引发 PropertyChanged — 无需为 [ObservableProperty] 字段手动取消订阅。订阅外部可观察对象或事件时,请实现 IDisposable:
public partial class MyViewModel : ObservableObject, IDisposable
{
private readonly IDisposable _subscription;
public MyViewModel(IMyService service)
{
_subscription = service.ValueStream.Subscribe(OnValue);
}
private void OnValue(string v) { }
public void Dispose() => _subscription.Dispose();
}
空值安全
XerahS 使用严格的可空引用类型。请始终:
// Enable in .csproj
<Nullable>enable</Nullable>
// Handle nullability properly
public string? Title { get; set; } // Nullable
public string Name { get; set; } = string.Empty; // Non-nullable with default
AOT 与裁剪注意事项
对于面向移动平台(iOS/Android)、WebAssembly 或 NativeAOT 的应用:
- 优先使用编译绑定(已是默认设置)— 它们可避免反射。
- 避免使用
Activator.CreateInstance解析 ViewModel/服务;应使用支持 AOT 的 DI 容器(例如,使用源生成注册的Microsoft.Extensions.DependencyInjection)。 - 避免在热点路径中使用
Type.GetType()、PropertyInfo.SetValue()或运行时反射。 - 无法避免反射时,使用
[DynamicallyAccessedMembers]注解。 - 谨慎使用
{ReflectionBinding}— 它不具备裁剪安全性,且不要将其与 Avalonia#ElementName路径组合使用。
开发者工具
⚠️ 旧版
Avalonia.Diagnostics包已弃用。切勿使用。
使用 migrate_diagnostics MCP 工具在任何项目中设置或迁移开发者工具。它会处理以下事项:
- 移除
Avalonia.Diagnostics - 安装
AvaloniaUI.DiagnosticsSupport - 配置
Program.cs和App.axaml.cs - 替换旧 API 调用
# Install the global developer tools CLI
dotnet tool install --global AvaloniaUI.DeveloperTools
规则:始终使用 AvaloniaUI.DiagnosticsSupport 和 AvaloniaUI.DeveloperTools .NET 全局工具。
常见错误及避免方法
| # | 错误 | 正确做法 |
|---|---|---|
| 1 | 使用 .xaml 扩展名 | 使用 .axaml |
| 2 | 使用 WPF 命名空间 | 使用 https://github.com/avaloniaui |
| 3 | 使用 Style.Triggers / DataTrigger / EventTrigger | 使用伪类选择器 |
| 4 | 使用 DependencyProperty | 使用 StyledProperty 或 DirectProperty |
| 5 | 使用 Style x:Key="..." | 使用样式类和选择器 |
| 6 | 使用 HierarchicalDataTemplate | 使用 TreeDataTemplate |
| 7 | 使用 pack://application:,,,/ URI | 使用 avares://AssemblyName/path |
| 8 | 使用 Visibility 枚举 | 使用 bool IsVisible(隐藏但保留空间时使用 Opacity="0") |
| 9 | 缺少 x:DataType | 编译绑定时始终设置它 |
| 10 | 默认使用 {ReflectionBinding} | 全局启用编译绑定 |
| 11 | 使用 {ReflectionBinding #MyRoot.SomeCommand} | 使用 {Binding #MyRoot.SomeCommand}(或编译绑定作用域) |
| 12 | 使用 Avalonia.Diagnostics | 使用 AvaloniaUI.DiagnosticsSupport + AvaloniaUI.DeveloperTools |
| 13 | 使用 ReactiveUI / Avalonia.ReactiveUI | 使用 CommunityToolkit.Mvvm |
| 14 | 手动交换 ContentControl.Content 页面 | 使用 NavigationPage / TabbedPage / DrawerPage |
| 15 | 使用 VisualStateManager | 使用伪类选择器或容器查询 |
| 16 | 使用 LayoutTransform | 使用 LayoutTransformControl 包裹 |
| 17 | 使用 Dispatcher.Invoke() | 使用 Dispatcher.UIThread.InvokeAsync() |
| 18 | 在 FluentAvalonia 中使用 ContextMenu | 使用 ContextFlyout + MenuFlyout |
| 19 | 在 StackPanel 外包裹 ScrollViewer Padding="N" | 将内边距移到内部元素:<StackPanel Margin="N"> — ScrollViewer.Padding 只会缩小视口,不会缩小滚动范围;底部内容会始终无法访问 |
| 20 | 将 SplitView 用作无窗格的双列外壳 | 使用 Grid ColumnDefinitions="auto,*" — SplitView 继承自 ContentControl,其默认 VerticalContentAlignment=Top 会将 ∞ 高度传递给子元素,导致任何嵌套的 ScrollViewer 无法正常工作 |
| 21 | 将包含 ScrollViewer 的 TransitioningContentControl 用作页面宿主 | 使用普通的 ContentControl HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" — TransitioningContentControl 的动画面板在测量期间会传递 ∞ 高度 |
| 22 | 使用默认 VerticalContentAlignment 的 TabControl 包裹可滚动选项卡 | 在 TabControl 上设置 VerticalContentAlignment="Stretch" — 内部 ContentPresenter 模板使用 {TemplateBinding VerticalContentAlignment},默认值为 Top,会将 ∞ 高度传递给选项卡主体 |
XerahS 中的常见模式
StyledProperty 模式
public static readonly StyledProperty<object?> SelectedObjectProperty =
AvaloniaProperty.Register<PropertyGrid, object?>(nameof(SelectedObject));
public object? SelectedObject
{
get => GetValue(SelectedObjectProperty);
set => SetValue(SelectedObjectProperty, value);
}
附加属性模式(审计)
public static readonly AttachedProperty<bool> IsUnwiredProperty =
AvaloniaProperty.RegisterAttached<UiAudit, Control, bool>("IsUnwired");
public static bool GetIsUnwired(Control control)
=> control.GetValue(IsUnwiredProperty);
public static void SetIsUnwired(Control control, bool value)
=> control.SetValue(IsUnwiredProperty, value);
窗口结构
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:XerahS.ViewModels"
x:Class="XerahS.UI.Views.MainWindow"
x:DataType="vm:MainViewModel"
x:CompileBindings="True"
Title="{Binding Title}"
Width="1000" Height="700">
<Window.Styles>
</Window.Styles>
<DockPanel>
</DockPanel>
</Window>
快速参考表
对齐值
| 属性 | 值 | 默认值 |
|---|---|---|
HorizontalAlignment | Left、Center、Right、Stretch | Stretch |
VerticalAlignment | Top、Center、Bottom、Stretch | Stretch |
绑定模式
| 模式 | 方向 | 更新时机 |
|---|---|---|
OneWay | 源 → 目标 | 源发生变化时 |
TwoWay | 源 ↔ 目标 | 双方发生变化时 |
OneTime | 源 → 目标 | 初始化时一次 |
OneWayToSource | 源 ← 目标 | 目标发生变化时 |
网格尺寸
| 类型 | 语法 | 行为 |
|---|---|---|
| Auto | Auto | 根据内容调整大小 |
| Pixel | 100 | 固定大小 |
| Star | * 或 2* | 按比例填充 |
其他资源
- 官方文档:https://docs.avaloniaui.net/
- GitHub:https://github.com/AvaloniaUI/Avalonia
- 示例:https://github.com/AvaloniaUI/Avalonia/tree/master/samples
- CommunityToolkit.Mvvm:https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/
- FluentAvalonia:https://github.com/amwx/FluentAvalonia
- 社区:https://avaloniaui.community/
新控件/视图检查清单
- 使用
.axaml文件扩展名 - 设置
x:Class属性 - 为编译绑定设置
x:DataType - 设置
x:CompileBindings="True"(或在.csproj中全局启用) - 定义正确的命名空间
- 可设置样式的自定义属性使用
StyledProperty;不可设置样式或性能关键的属性使用DirectProperty - 遵循可空引用类型规则
- 使用
CommunityToolkit.Mvvm(ObservableObject、[ObservableProperty]、[RelayCommand])实现 MVVM——不要使用 ReactiveUI - 将 ViewModel 类标记为
partial,以支持源生成器 - 使用一致的样式/主题
- ⚠️ 使用
ContextFlyout+MenuFlyout,不要使用ContextMenu(兼容 FluentAvalonia) - 在 DataTemplates 中为弹出菜单绑定使用
$parent[UserControl].DataContext - 对
#ElementName路径使用 AvaloniaBinding;切勿使用{ReflectionBinding #...} - 多页面应用使用
NavigationPage/TabbedPage/DrawerPage,不要手动切换ContentControl - AOT/裁剪:优先使用编译绑定;避免运行时反射
- 使用
AvaloniaUI.DiagnosticsSupport——切勿使用Avalonia.Diagnostics - 处理无障碍访问(Tab 顺序、可访问名称)
- 在所有目标平台上测试
最后更新:2026 年 3 月 17 日
版本:1.3.1
维护者:XerahS 开发团队