多平台 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日:文章更新

