多平台 Avalonia .NET 框架 XAML 基础知识轻松示例






4.84/5 (16投票s)
本文描述了 Avalonia XAML 的基本功能,并提供了易于理解的示例。
请注意,代码已迁移到 NP.Ava.Demos 存储库,所有示例均已升级,可在 Avalonia 11.0.6 (最新稳定版 Avalonia) 下运行
引言
本文是 Avalonia 系列文章的第三篇,前两篇为使用 AvaloniaUI 进行多平台 UI 编码:简单示例。第 1 部分 - AvaloniaUI 构建块和多平台 Avalonia .NET 框架编程基本概念:简单示例。
本文是 Avalonia XAML 基本功能的指南。除了设置 Visual Studio 和创建 Avalonia 项目的说明之外,您无需阅读前两篇文章即可理解本文。
本文假定读者对 XML 和 C# 有一些基本了解。
Avalonia 是一个很棒的新开源软件包,与 WPF 非常相似,但与 WPF 或 UWP 不同,它适用于大多数平台——Windows、macOS 和各种 Linux 版本,并且在许多方面都比 WPF 更强大。
对于构建多平台桌面应用程序,Avalonia 也比 Node.js 或 Xamarin 更强大、更灵活。
Avalonia 的源代码可在Avalonia 源代码中找到。
本文内容涵盖 Avalonia XAML 的基础知识,例如命名空间、类型、属性、资源、泛型和面向初学者的基本标记扩展。这里,我们不会深入探讨附加属性或绑定如何在 XAML 中表示(这已在多平台 Avalonia .NET 框架编程基本概念:简单示例中介绍过),也不会涉及模板和样式(这将在未来的文章中介绍)。
尽管本文中的所有示例都涉及 Avalonia,但其中许多内容也适用于其他 XAML 框架,如 WPF、UWP 和 Xamarin。
请注意,我最近(通过以下出色的演示TypeArgumentsDemo)发现,Visual Studio 2019 的 Avalonia 扩展支持已编译 XAML 的泛型——这是 WPF 团队很久以前承诺添加的功能,但据我所知,他们从未实现。我将在本文中专门用一节介绍 Avalonia XAML 中的泛型。
我还将本文的大部分材料和示例用于新的 Avalonia 文档,该文档即将发布在Avalonia Documentation上。
本文的所有代码都位于 Github NP.Ava.Demos 存储库下的 NP.Ava.Demos/NP.Demos.XamlSamples 中。
什么是 XAML
XAML 是用于构建 C#(主要是可视)对象的 XML。
C# 类显示为 XML 标签,而类属性通常显示为 XML 属性
<my_namespace:MyClass Prop1="Hello World"
Prop2="123"/>
上面示例中的 XAML 代码创建了一个来自 XML 命名空间 my_namespace
的 MyClass
类型对象,并将其属性 Prop1
设置为字符串 "Hello World
",Prop2
设置为值 123
。请注意,属性将解析为其 C# 类型,例如,如果 Prop2
是 int
类型,它将解析为整数值 123;如果它是 string
类型,它将解析为字符串 "123
"。如果 XAML 文件中提到的属性或类型不存在,包含该 XAML 的项目编译将失败,并且 Visual Studio 通常会在编译之前检测到错误,并用红色的波浪线标出缺失的类型或属性。
命名空间(在我们的示例中是 "my_namespace
")通常应在 XML 标签上方或内部定义,它可以在那里使用。它可以指向一个 C# 命名空间或一组 C# 命名空间(如下面将通过适当的示例解释)。
XAML 文件可以与一个名为“代码隐藏”的 C# 文件关联,以使用“部分类”声明定义相同的类。C# 代码隐藏通常包含方法的定义,这些方法用作 XAML 文件中定义的元素触发的事件的事件处理程序。这种将 XAML 元素触发的事件与 C# 事件处理程序关联起来的方式是最简单、最直接的,并且已在上一篇文章的使用 Visual Studio 2019 创建和运行简单的 Avalonia 项目部分中解释过。然而,这也是最糟糕的,因为它破坏了重要的 MVVM 模式(将在未来的文章中展示),并且几乎不应使用。
XAML 命名空间示例
XAML 命名空间是一个通常在 XAML 文件的顶层元素(尽管它可以在任何标签上定义)中定义的 string
,它指向当前 XAML 文件所在项目所依赖的某些 .NET 程序集中的某些 C# 命名空间。
查看我们第一篇文章的入门示例 MainWindow.xaml 文件的前两行
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" ... >
这两行为整个文件定义了两个 XAML 命名空间。其中一个命名空间不需要任何前缀(它有一个空前缀),另一个命名空间有前缀 "x
"。这两个命名空间都引用了 Avalonia 包中定义的类型。您可以在 Avalonia 中定义许多元素(例如一个 button
),而无需任何前缀(例如,作为 <Button .../>
),因为这些元素位于由 "https://github.com/avaloniaui" URL 引用的默认 Avalonia 命名空间中。由前缀 "x" 引用的命名空间包含各种使用频率稍低的类型。例如,许多内置的 C# 类型,例如 string
和 object
,可以在 XAML 中表示为 <x:String>...</x:String>
和 <x:Object>...</x:Object>
(它们包含在由 "http://schemas.microsoft.com/winfx/2006/xaml" URL 引用的 Avalonia 命名空间中)。
重要提示:所谓的 XAML 命名空间 URL 不必指向任何真实存在的有效 URL,并且计算机不必在线即可使其工作。
显示定义自定义 XAML 命名空间的各种方式的示例位于 NP.Demos.XamlNamespacesSample.sln 解决方案下。从 Github 下载其代码并在 Visual Studio(或 Rider)中打开解决方案。
您可以看到,该解决方案由三个项目组成:主项目 NP.Demos.XamlNamespacesSample
和主项目依赖的两个项目:Dependency1Proj
和 Dependency2Proj
编译并运行解决方案——您将看到以下内容
窗口中垂直堆叠了五个不同颜色的方形按钮。
以下是 MainWindow.xaml 文件的相关代码
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:dep1_sub_Folder="clr-namespace:Dependency1Proj.SubFolder;
assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
xmlns:dep2="https://avaloniademos.com/xaml"
...>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<dep1:RedButton/>
<dep1_sub_Folder:BlueButton/>
<local:BrownButton/>
<dep2:GreenButton/>
<dep2:GrayButton/>
</StackPanel>
</Window>
顶层 XML 标签的以下行定义了四个自定义命名空间
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:dep1_test_Folder="clr-namespace:Dependency1Proj.SubFolder;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
xmlns:dep2="https://avaloniademos.com/xaml"
不同的按钮由相应的 XAML 命名空间前缀引用。让我们看看 <RedButton/>
。<RedButton/>
类在 Dependency1Proj
项目下定义为 C# 类。这是它的代码
namespace Dependency1Proj
{
public class RedButton : Button, IStyleable
{
Type IStyleable.StyleKey => typeof(Button);
public RedButton()
{
Background = new SolidColorBrush(Colors.Red);
Width = 30;
Height = 30;
}
}
}
这一行...
Type IStyleable.StyleKey => typeof(Button);
...(以及 RedButton
类实现 IStyleable
接口的事实)确保主主题的默认按钮样式也将应用于派生自 Avalonia Button
类的 RedButton
类。按钮的构造函数将按钮颜色指定为红色,并将按钮的高度和宽度设置为 30 个通用像素。
请注意,示例中每个按钮的代码与 RedButton
的代码完全相同,只是按钮类名、C# 命名空间和分配给按钮的颜色不同。
现在看看定义 XAML 命名空间前缀 dep1
的行,我们通过它在 XAML 文件中引用此按钮
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
命名空间的值包含两部分,由“;
”分号分隔。第一部分指 C# 命名空间
clr-namespace:Dependency1Proj
第二部分指程序集名称
assembly=Dependency1Proj
对于 RedButton
,命名空间和程序集名称都相同:Dependency1Proj
。
BlueButton
定义在同一个项目 (Dependency1Proj
) 中,但位于 SubFolder 文件夹内。它的 C# 命名空间不是 Dependency1Proj
(如 RedButton
),而是 Dependency1Proj.SubFolder
。
这是定义 XAML 命名空间前缀 dep1_sub_Folder
的行,BlueButton
在 MainWindow.xaml 文件中通过它引用
xmlns:dep1_sub_Folder="clr-namespace:Dependency1Proj.SubFolder;assembly=Dependency1Proj"
由于 BlueButton
定义在相同的 Dependency1Proj
程序集中,因此 clr-namespace
更改为 Dependency1Proj.SubFolder
,而程序集部分保持不变。
现在来看看 <local:BrownButton\>
。BrownButton
的 C# 代码定义在主项目 NP.Demost.XamlNamespacesSample
中——即 MainWindow.xaml 文件所在的同一项目。因此,在定义前缀 "local
"(我们的 BrownButton
所引用的)时,我们可以省略程序集名称,只指定 clr-namespace
部分
xmlns:local="clr-namespace:NP.Demos.XamlNamespacesSample"
GreenButton
和 GrayButton
定义在 Dependency2Proj
的两个不同命名空间中。GreenButton
定义在项目的主命名空间 Dependency2Proj
下,而 GrayButton
定义在 Dependency2Proj.SubFolder
命名空间下。然而,Dependency2Proj
也有一个 AssemblyInfo.cs 文件,其中定义了程序集元数据。在这个文件中,我们在底部添加了几行
[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj")]
[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj.SubFolder")]
这两行将程序集的两个命名空间:Dependency2Proj
和 Dependency2Proj.SubFolder
合并到同一个 URL:“https://avaloniademos.com/xaml”。如上所述,该 URL 是否存在或计算机是否在线都无关紧要。如果您的 URL 能够传达与包含此功能的项目相对应的某种含义,那将是很好的。
现在,我们用来引用 GreenButton
和 GrayButton
的 XAML 前缀 dep2
是通过引用该 URL 定义的
xmlns:dep2="https://avaloniademos.com/xaml"
与 WPF 相比,Avalonia 有一个重要的额外功能。在 WPF 中,只能通过 URL 引用不位于与要引用它的 XAML 文件相同的项目中的功能,而在 Avalonia 中,没有这种限制——例如,如果我们在同一个 Dependency2Proj
项目中有一个 XAML 文件,我们仍然可以添加一行...
xmlns:dep2="https://avaloniademos.com/xaml"
...在其顶部元素处,并通过 dep2:
前缀引用在同一项目中定义的 GreenButton
和 GrayButton
。
在 XAML 中访问 C# 复合属性
我们前面已经提到,C# 内置属性可以作为相应元素的 XML 属性访问,例如
<my_namespace:MyClass Prop1="Hello World"
Prop2="123"/>
Prop1
和 Prop2
是在 MyClass
类上定义的简单 C# 属性,该类可以在该 XAML 文件的 my_namespace
前缀所引用的 C# 命名空间中找到。Prop1
可能是 string
类型,而 Prop2
可以是任何数值类型或 string
(XAML 将自动将字符串 "123
" 转换为正确的类型)。
然而,如果属性本身是某种包含其自身几个属性的复杂类型,会发生什么呢?
C# 解决方案 NP.Demos.AccessPropertiesInXamlSample.sln 展示了如何在 XAML 中创建此类属性。
项目中定义了一个 Person
类
namespace NP.Demos.AccessPropertiesInXamlSample
{
public class Person
{
public int AgeInYears { get; set; }
public string? FirstName { get; set; }
public string? LastName { get; set; }
public override string ToString()
{
return $"Person: {FirstName} {LastName}, Age: {AgeInYears}";
}
}
}
我们想将其显示为窗口内容。这是我们在 MainWindow.xaml 文件中的内容
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NP.Demos.AccessPropertiesInXamlSample"
x:Class="NP.Demos.AccessPropertiesInXamlSample.MainWindow"
Width="300"
Height="200"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Title="AccessPropertiesInXamlSample">
<Window.Content>
<local:Person FirstName="Joe"
LastName="Doe"
AgeInYears="25"/>
</Window.Content>
</Window>
请注意我们将 Window
的 Content
属性分配给复合 Type
的方式
<Window ...>
<Window.Content>
<local:Person FirstName="Joe"
LastName="Doe"
AgeInYears="25"/>
</Window.Content>
</Window>
我们使用 Window.Content
属性标签,用句点分隔类名和属性名。
请注意,与我们分配复合类型属性的方式相同,我们也可以分配原始类型属性,例如,我们可以通过以下代码设置窗口的 Width
<Window ...>
<Window.Width>
<x:Double>300</x:Double>
</Window.Width>
</Window>
而不是使用 XML 属性。当然,这种表示方式比 XAML 属性表示方式笨重得多,因此很少用于原始类型属性。
注意:因为 Window.Content
是一个由 ContentAttribte
标记的特殊属性,所以我们根本不需要添加 <Window.Content>
,可以直接将 <local:Person .../>
对象放在 <Window...>
标签下。每个类只有一个属性可以被 ContentAttribute
标记,所以在很多情况下,我们仍然被迫使用 <Class.Property
记法。
XAML 特殊属性
有几个用前缀 "x:
" 标记的特殊属性,当然前提是我们在文件顶部定义了 "x
" 命名空间前缀为
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
其中最重要的是 x:Name
和 x:Key
。
x:Name
用于 XAML 树中的元素,以便能够轻松地在 C# 中查找元素,也(被一些人)用于为 XAML 提供一些自我文档,并能够轻松地在 Avalonia 开发工具中识别元素。
我们已经在使用 AvaloniaUI 进行多平台 UI 编码:简单示例。“使用 Visual Studio 2019 创建和运行简单的 AvaloniaUI 项目”部分中展示了如何在 C# 代码中查找 x:Name
命名元素:可以使用 FindControl(...)
方法,例如,对于 XAML 中定义并命名为 "CloseWindowButton
" 的按钮,我们可以在代码隐藏中使用以下方法查找它
var button = this.FindControl<button>("CloseWindowButton");</button>
x:Key
用于查找 Avalonia XAML 资源,我们将在专门介绍它们的章节中解释。
标记扩展的简要介绍
标记扩展是一些可以显著简化 XAML 的 C# 类。它们用于使用带有花括号('{
' 和 '}
')的单行表示法设置某些 XAML 属性。有一些非常重要的内置 Avalonia 标记扩展——最重要的是以下这些
StaticResource
DynamicResource
x:Static
数据绑定。
我们将为所有这些提供示例,除了 Binding(已在绑定中解释)。
也可以创建自定义标记扩展,但这很少使用,本指南将不涉及。我们将在未来的指南之一中解释它。
Avalonia XAML 资源
XAML 资源是重用 XAML 代码并将一些通用 XAML 代码放置在通用 Visual 项目中以供多个应用程序使用的最重要方法之一。
StaticResource vs DynamicResource 示例
此示例显示了静态资源和动态资源之间的主要区别:当资源本身更新时,静态资源目标值不会更新,而动态资源则会更新。
该示例位于 NP.Demos.StaticVsDynamicXamlResourcesSample 解决方案下。
打开解决方案并运行它,您将看到
按“更改状态颜色”按钮将导致第三个矩形将其颜色切换为红色
以下是示例的 MainWindow.xaml 文件
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NP.Demos.StaticVsDynamicXamlResourcesSample.MainWindow"
Title="NP.Demos.StaticVsDynamicXamlResourcesSample"
Width="300"
Height="200">
<Window.Resources>
<ResourceDictionary>
<!--We set the XAML resource-->
<SolidColorBrush x:Key="StatusBrush"
Color="Green"/>
</ResourceDictionary>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel x:Name="ElementsPanel"
Orientation="Vertical">
<!--Refer to xaml resource using StaticResource Markup Expression -->
<Border x:Name="Border1"
Background="{StaticResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
<!--Refer to xaml resource using StaticResource (without markup expression) -->
<Border x:Name="Border2"
Height="30"
Width="80"
Margin="0,5">
<Border.Background>
<StaticResource ResourceKey="StatusBrush"/>
</Border.Background>
</Border>
<!--Refer to xaml resource using DynamicResource Markup Expression -->
<Border x:Name="StatusChangingBorder"
Background="{DynamicResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
</StackPanel>
<Button x:Name="ChangeStatusButton"
Grid.Row="1"
Width="160"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
Content="Change Status Color"
Margin="10"/>
</Grid>
</Window>
我们在与 MainWindow.xaml 文件相同的文件中将 XAML 资源定义为窗口的资源
<Window.Resources>
<ResourceDictionary>
<!--We set the XAML resource-->
<SolidColorBrush x:Key="StatusBrush"
Color="Green"/>
</ResourceDictionary>
</Window.Resources>
XAML 资源的 x:Key
可以被 StaticResource
和 DynamicResource
用来引用特定的资源。
然后,我们使用 StaticResource
设置前两个边框的背景,并使用 DynamicResource
设置垂直边框堆栈中的第三个边框。
对于堆栈中的第一个边框,我们使用 StaticResource
标记扩展
<!--Refer to xaml resource using StaticResource Markup Expression -->
<Border x:Name="Border1"
Background="{StaticResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
对于第二个边框,我们使用 StaticResource
类,没有标记扩展(您可以看到相应的 XAML 冗余得多)
<Border x:Name="Border2"
Height="30"
Width="80"
Margin="0,5">
<Border.Background>
<StaticResource ResourceKey="StatusBrush"/>
</Border.Background>
</Border>
最后,第三个边框使用 DynamicResource
标记扩展
<!--Refer to xaml resource using DynamicResource Markup Expression -->
<Border x:Name="StatusChangingBorder"
Background="{DynamicResource StatusBrush}"
Height="30"
Width="80"
Margin="0,5"/>
“StatusChangingBorder
”按钮在 MainWindow.xaml.cs 文件中被连接以将“StatusBrush
”资源从“Green
”更改为“Red
”
public MainWindow()
{
InitializeComponent();
Button button =
this.FindControl<Button>("ChangeStatusButton");
button.Click += Button_Click;
}
private void Button_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
// getting a Window resource by its name
var statusBrush = this.FindResource("StatusBrush");
// setting the window resource to a new value
this.Resources["StatusBrush"] =
new SolidColorBrush(Colors.Red);
}
尽管所有三个边框的资源相同,但只有最后一个边框的背景会改变——即使用 DynamicResource
的那个。
静态资源和动态资源之间的其他重要区别如下
DynamicResource
可以引用在DynamicResource
表达式下方定义的 XAML 资源,而StaticResource
应该引用其上方的资源。StaticResource
可用于为 XAML 中使用的各种对象上的简单 C# 属性赋值,而DynamicResource
语句的目标应始终是AvaloniaObject
上的特殊 Avalonia 属性(特殊属性已在附加属性中解释)。- 由于
DynamicResource
功能更强大(提供更改通知),因此它比StaticResource
占用更多的内存资源。因此,当您不需要更改通知时(属性在程序运行期间保持不变),应始终使用StaticResource
。DynamicResources
在您想要动态更改应用程序主题或颜色时非常有用,例如,允许用户切换主题或根据一天中的时间更改颜色。
引用不同 XAML 文件和项目中的 XAML 资源示例
在本示例中,我们将展示如何引用位于同一项目或不同项目中的不同文件中的 XAML 资源。
该示例位于 NP.Demos.XamlResourcesInMultipleProjects Visual Studio 解决方案下。运行示例后,您将看到三个不同颜色的矩形——红色、绿色和蓝色
该解决方案由两个项目组成——主项目 NP.Demos.XamlResourcesInMultipleProjects
和主项目依赖的另一个项目 Dependency1Proj
RedBrush
资源定义在 Dependency1Proj
下的 Themes/BrushResources.axaml 文件中
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<SolidColorBrush x:Key="GreenBrush"
Color="Green"/>
</ResourceDictionary>
请注意,BrushResources.axaml 文件具有“Avalonia XAML”构建操作(任何 Avalonia XAML 资源文件都应该如此)
通过为 Visual Studio 新建项创建选择“资源字典 (Avalonia)”模板来创建此类文件
GreenBrush
Avalonia 资源定义在 Themes/LocalBrushResources.axaml 文件中(此文件位于主项目中)
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<SolidColorBrush x:Key="GreenBrush"
Color="Green"/>
</ResourceDictionary>
这是 MainWindow.axaml 文件的内容
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="NP.Demos.XamlResourcesInMultipleProjects.MainWindow"
Title="NP.Demos.XamlResourcesInMultipleProjects"
Width="100"
Height="180">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Dependency1Proj/Themes/BrushResources.axaml"/>
<ResourceInclude Source="/Themes/LocalBrushResources.axaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- BlueBrush is defined locally -->
<SolidColorBrush x:Key="BlueBrush"
Color="Blue"/>
</ResourceDictionary>
</Window.Resources>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border x:Name="RedBorder"
Width="70"
Height="30"
Background="{StaticResource RedBrush}"
Margin="5"/>
<Border x:Name="GreenBorder"
Width="70"
Height="30"
Background="{StaticResource GreenBrush}"
Margin="5"/>
<Border x:Name="BlueBorder"
Width="70"
Height="30"
Background="{StaticResource BlueBrush}"
Margin="5"/>
</StackPanel>
</Window>
我们有三个垂直堆叠的边框——第一个边框的背景值来自 RedBrush
资源,第二个边框来自 GreenBrush
,第三个边框来自 BlueBrush
。
查看文件顶部的窗口 Resources
部分
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://Dependency1Proj/Themes/BrushResources.axaml"/>
<ResourceInclude Source="/Themes/LocalBrushResources.axaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- BlueBrush is defined locally -->
<SolidColorBrush x:Key="BlueBrush"
Color="Blue"/>
</ResourceDictionary>
</Window.Resources>
<ResourceDictionary.MergedDictionary>
标签内的 <ResourceInclude .../>
标签表示我们将外部定义的资源字典合并到当前字典中——这样我们就可以获取它们所有的键值对。了解 WPF 的人可能会注意到其中的区别——在 WPF 中,我们使用 <ResourceDictionary Source="..."/>
标签而不是 <ResourceInclude Source="..."/>
。另请注意,对于依赖项目,我们没有将程序集与 URL 的其余部分分开,也没有使用神秘的“Component/”前缀作为 URL。这些纯粹是符号上的(而不是概念上的)差异,但仍需要记住。
注意合并文件的 Avalonia XAML URL
- "avares://Dependency1Proj/Themes/BrushResources.axaml" - 在不同项目中定义的 Avalonia XAML 资源文件的 URL 应以魔术词 "avares://" 开头,后跟程序集名称,再后跟文件路径:"avares://<assembly-name>/<path-to-the-avalonia-resource_file>。
- "/Themes/LocalBrushResources.axaml" - 在同一项目(即正在使用的项目)中定义的 Avalonia XAML 资源文件的 URL,应仅包含一个斜杠,后跟从当前项目根目录到 Avalonia 资源文件的路径。
在资源部分的末尾,我们定义了 BlueBrush
资源——它对 MainWindow.axaml 文件是局部的。
x:Static 标记扩展
x:Static
标记扩展允许引用在同一项目或某些依赖项目中定义的 static
属性。示例代码位于 NP.Demos.XStaticMarkupExtensionSample 解决方案下。它包含两个项目——NP.Demos.XStaticMarkupExtensionSample
(主项目)和依赖项目 Dependency1Proj
。主项目包含类 LocalProjectStaticBrushes
,而依赖项目包含 DependencyProjectStaticBrushes
。
两个 C# 文件的内容都非常简单——每个文件都定义并设置一个 static
属性的值。这是 LocalProjectStaticBrushes
类的内容
public class LocalProjectStaticBrushes
{
public static Brush GreenBrush { get; set; } =
new SolidColorBrush(Colors.Green);
}
这是 DependencyProjectStaticBrushes
类的内容
public class DependencyProjectStaticBrushes
{
public static Brush RedBrush { get; set; } =
new SolidColorBrush(Colors.Red);
}
运行项目将创建一个带有两个矩形(红色和绿色)的窗口
以下是 MainWindow.axaml 文件的相关部分
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XStaticMarkupExtensionSample"
...>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border Width="70"
Height="30"
Background="{x:Static dep1:DependencyProjectStaticBrushes.RedBrush}"
Margin="5" />
<Border Width="70"
Height="30"
Background="{x:Static local:LocalProjectStaticBrushes.GreenBrush}"
Margin="5" />
</StackPanel>
</Window>
在 Window
标签级别定义了两个重要的命名空间
xmlns:dep1="clr-namespace:Dependency1Proj;assembly=Dependency1Proj"
xmlns:local="clr-namespace:NP.Demos.XStaticMarkupExtensionSample"
"dep1
" 对应于依赖项目,"local
" 对应于 MainWindow.xaml 文件本地的项目(主项目)。
使用这些命名空间前缀和 x:Static
标记扩展,我们可以设置两个边框上的 Background
属性
Background="{x:Static dep1:DependencyProjectStaticBrushes.RedBrush}"
和
Background="{x:Static local:LocalProjectStaticBrushes.GreenBrush}"
Avalonia XAML 中的泛型
正如我上面提到的,我从以下出色的演示TypeArgumentsDemo中了解到 Visual Studio 2019 编译的 Avalonia XAML 支持泛型。据我所知,微软从未为 WPF 添加过此类功能,尽管他们曾一度打算这样做。
泛型演示位于 XamlGenericsSamples 项目下。
有一个带有两个泛型类型参数的 ValuesContainer
类
public class ValuesContainer<TVal1, TVal2>
{
public TVal1? Value1 { get; set; }
public TVal2? Value2 { get; set; }
}
ValuesContainer
定义了两个值:泛型类型 TVal1
的 Value1
和泛型类型 TVal2
的 Value2
。
其余有趣的代码都位于 MainWindow.axaml 文件中。
运行示例,您将看到以下内容
有三个示例——第一个解释如何创建单个 ValuesContainer
对象,第二个解释如何创建 ValuesContainer
对象列表,第三个解释如何创建将整数映射到 ValuesContainer
对象的 Dictionary
。让我们逐一解释这些示例。
单个 ValuesContainer 对象示例
这是此示例的代码
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<local:ValuesContainer x:TypeArguments="x:Double, x:String"
Value1="300"
Value2="Hello 1"/>
</Grid.DataContext>
<TextBlock Text="ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<Grid Background="Yellow"
Grid.Row="1"
RowDefinitions="Auto, Auto"
Width ="{Binding Path=Value1}"
HorizontalAlignment="Left">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Width of Yellow Rectangle=Value1=\{0\}'}"
Margin="5"/>
<TextBlock Text="{Binding Path=Value2, StringFormat='Value2=\'\{0\}\''}"
Grid.Row="1"
Margin="5,0,5,5"/>
</Grid>
</Grid>
我们将 ValuesContainer
对象定义为包含示例代码的 Grid
的 DataContext
<Grid.DataContext>
<local:ValuesContainer x:TypeArguments="x:Double, x:String"
Value1="300"
Value2="Hello 1"/>
</Grid.DataContext>
ValuesContainer
对象的 x:TypeArguments
属性定义了一个以逗号分隔的泛型类型参数列表——我们将第一个参数定义为 double
,第二个定义为 string
。然后我们设置 Value1="300"
和 Value2="Hello 1"
。请注意,XML 字符串 "300
" 将自动转换为 double
。由于 DataContext
是一个特殊的属性,它会在可视化树中向下传播,因此相同的 DataContext
将在 Grid
的所有后代上定义。我们可以将后代 TextBlocks
的 Text
属性绑定到 Value1
和 Value2
以显示这些值。此外,为了证明 Value1
确实是 double
类型(而不是 string
),我们将内部网格(带黄色背景)的宽度绑定到 DataContext
的 Value1
属性
<Grid Background="Yellow"
...
Width ="{Binding Path=Value1}" ...>
这样黄色矩形的宽度将为 300。
ValuesContainer 对象列表示例
在查看示例的 XAML 代码之前,请注意我们在 XAML 文件顶部定义了一个命名空间 collections
:xmlns:collections="clr-namespace:System.Collections.Generic;assembly=System.Collections"
。此命名空间指向定义泛型集合(如 List<...>
和 Dictionary<...>
)的 C# 命名空间和程序集。
这是相应的 XAML 代码
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="3"
Value2="Hello 3"/>
</collections:List>
</Grid.DataContext>
<TextBlock Text="List of ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Value1=\{0\}'}"/>
<TextBlock Text="{Binding Path=Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
这次,容器的 DataContext
被定义为 List<ValuesContainer<int, string>>
,即我们有两层类型参数递归
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
...
</collections:List>
然后,由于 List<...>
有一个 Add
方法,我们可以简单地在 List<...>
中添加单个对象
<collections:List x:TypeArguments="local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
Value1="3"
Value2="Hello 3"/>
</collections:List>
请注意,对于每个 ValuesContainer
对象,Value1
将自动转换为 int
。
然后我们将 ItemsControl
的 Items
属性绑定到列表,并使用 ItemsControl
的 ItemTemplate
来显示每个单独项目的 Value1
和 Value2
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Value1,
StringFormat='Value1=\{0\}'}"/>
<TextBlock Text="{Binding Path=Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
整数到 ValuesContainer 对象字典示例
我们的最后一个示例更有趣。我们展示了如何创建和显示泛型 Dictionary<string, ValuesContainer<int, string>>
的上下文。
以下是该示例的相关代码
<Grid RowDefinitions="Auto, Auto">
<Grid.DataContext>
<collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key1"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key2"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key3"
Value1="3"
Value2="Hello 3"/>
</collections:Dictionary>
</Grid.DataContext>
<TextBlock Text="Dictionary of ValuesContainer Generics Sample:"
FontWeight="Bold"/>
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Key,
StringFormat='Key=\'\{0\}\''}"/>
<TextBlock Text="{Binding Path=Value.Value1,
StringFormat='Value1=\{0\}'}"
Margin="10,0,0,0"/>
<TextBlock Text="{Binding Path=Value.Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
以下是我们定义 Dictionary<string, ValuesContainer<int, string>>
的方式:<collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">
。
以下是字典填充方式
<collections:Dictionary x:TypeArguments="x:String, local:ValuesContainer(x:Int32, x:String)">
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key1"
Value1="1"
Value2="Hello 1"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key2"
Value1="2"
Value2="Hello 2"/>
<local:ValuesContainer x:TypeArguments="x:Int32, x:String"
x:Key="Key3"
Value1="3"
Value2="Hello 3"/>
</collections:Dictionary>
请注意,我们只需在 dictionary
中创建 ValuesContainer
对象,但每个对象都将 x:Key
属性设置为唯一值。此 x:Key
属性指定字典的键,而 ValuesContainer
对象成为值。请注意,在我们的案例中,dictionary
的键是 string
类型,但如果它是其他已知类型,例如 int
,Avalonia XAML 编译器会将键值转换为整数。
上述 XAML 代码用三个 KeyValuePair<string, ValuesContainer<int, string>>
对象填充我们的字典,其 json 表示如下
[
{ Key="Key1"
Value = new ValuesContainer{ Value1=1, Value2="Hello 1"},
{ Key="Key2"
Value = new ValuesContainer{ Value1=2, Value2="Hello 2"},
{ Key="Key3"
Value = new ValuesContainer{ Value1=3, Value2="Hello 3"}
]
以下是我们如何将 ItemsControl
绑定到这些对象并使用其 ItemTemplate
显示它们
<ItemsControl Items="{Binding}"
Grid.Row="1">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Key,
StringFormat='Key=\'\{0\}\''}"/>
<TextBlock Text="{Binding Path=Value.Value1,
StringFormat='Value1=\{0\}'}"
Margin="10,0,0,0"/>
<TextBlock Text="{Binding Path=Value.Value2,
StringFormat='Value2=\'\{0\}\''}"
Margin="10,0,0,0"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
第一个 TextBlock
的 Text
属性绑定到 KeyValuePair<...>
的 Key
,第二个绑定到 Value1
,第三个绑定到 Value2
。
在 XAML 中引用资产
在 Avalonia 术语中,资产通常是二进制图像(例如,png 或 jpg)文件。在本节中,我们将展示如何从 XAML 中的 Image
控件引用此类文件。
示例代码位于 NP.Demos.ReferringToAssetsInXaml NP.Demos.ReferringToAssetsInXaml
解决方案下。以下是解决方案的代码
我们在依赖项目 Dependency1Proj
下有一个 Themes/avalonia-32.png 文件,在主项目下有一个 Themes/LinuxIcon.jpg 文件。
请注意,资产文件的“生成操作”应为“AvaloniaResource
”(与 XAML 资源文件不同,我们看到它设置为“Avalonia XAML”)
构建并运行示例,您将看到以下内容
有四个垂直堆叠的图像——这是相应的代码
<Image Source="/Assets/LinuxIcon.jpg"
Width="50"
Height="50"
Margin="5"/>
<Image Source="avares://Dependency1Proj/Assets/avalonia-32.png"
Width="50"
Height="50"
Margin="5"/>
<Image x:Name="LinuxIconImage2"
Width="50"
Height="50"
Margin="5"/>
<Image x:Name="AvaloniaIconImage2"
Width="50"
Height="50"/>
前两个图像的 Source
在 XAML 中设置,最后一个在 C# 后端代码中设置。
请注意,被定义为与使用它的 MainWindow.axaml 文件在同一项目中的本地资产的图像可以使用简化版的源 URL
Source="/Assets/LinuxIcon.jpg"
而位于不同项目中的图像应使用带有 avares:// 前缀的完整 URL。
Source="avares://Dependency1Proj/Assets/avalonia-32.png"
请注意,与 XAML 资源字典文件的情况相同,并且与 WPF 不同,程序集名称(在本例中为 "Dependency1Proj
")是 URL 的一部分,并且没有 Component 前缀。
最后两个图像的 Source
属性在 MainWindow.axaml.cs 代码隐藏文件中设置。以下是相关代码
public MainWindow()
{
InitializeComponent();
...
// get the asset loader from Avalonia container
var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
// get the Image control from XAML
Image linuxIconImage2 = this.FindControl<Image>("LinuxIconImage2");
// set the image Source using assetLoader
linuxIconImage2.Source =
new Bitmap
(
assetLoader.Open(
new Uri("avares://NP.Demos.ReferringToAssetsInXaml/Assets/LinuxIcon.jpg")));
// get the Image control from XAML
Image avaloniaIconImage2 = this.FindControl<Image>("AvaloniaIconImage2");
// set the image Source using assetLoader
avaloniaIconImage2.Source =
new Bitmap
(
assetLoader.Open(
new Uri("avares://Dependency1Proj/Assets/avalonia-32.png")));
}
请注意,即使对于本地文件“LinuxIcon.jpg”(与使用它的 MainWindow.xaml.cs 文件在同一项目中定义的文件),我们也需要提供带有“avares://<assembly-name>/”前缀的完整 URL。
非可视化 XAML 代码
最后一个示例将演示,即使对于完全非可视的代码,也可以使用 XAML。该示例位于 NP.Demos.NonVisualXamlSample 解决方案下。与之前的示例不同,它是一个控制台应用程序,只引用了一个(而不是三个)Avalonia nuget 包
主程序位于 Program.cs 文件中,非常简单
public static void Main(string[] args)
{
Course course = new Course();
}
您可以在该行之后设置一个断点,并查看 course
对象的内容
查看 Course.axaml/Course.axaml.cs 文件。以下是 Course.axaml 文件的内容
<x:Object xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NP.Demos.NonVisualXamlSample"
x:Class="NP.Demos.NonVisualXamlSample.Course"
NumberStudents="5">
<local:Person FirstName="Joe" LastName="Doe" Age="100" />
</x:Object>
类型为 Course
的顶层标签包含一个类型为 Person
的单个对象。顶层对象的 NumberStudents
属性设置为 5
,而 Person
的属性设置为 FirstName="Joe"
、LastName="Doe"
和 Age="100"
。
Course.axaml.cs 文件定义了 Course
类的属性
public partial class Course
{
public Course()
{
AvaloniaXamlLoader.Load(this);
}
public int NumberStudents { get; set; }
[Content]
public Person? Teacher { get; set; }
}
请注意,它的构造函数也定义了加载 XAML 文件的方法,并且该类被标记为“partial
”(另一部分是从 XAML 生成的)。另请注意,Teacher
属性具有 ContentAttribute
——这就是为什么我们不需要使用 <local:Course.Teacher>
标签来放置 Person
对象。
最后,以下是 Person
类的代码
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public double Age { get; set; }
}
结论
本文通过示例详细解释了 XAML 的基本功能。
历史
- 2021年10月4日:初始版本
- 2023年12月22日:文章更新