65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (16投票s)

2021年10月4日

CPOL

21分钟阅读

viewsIcon

26624

本文描述了 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_namespaceMyClass 类型对象,并将其属性 Prop1 设置为字符串 "Hello World",Prop2 设置为值 123。请注意,属性将解析为其 C# 类型,例如,如果 Prop2int 类型,它将解析为整数值 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# 类型,例如 stringobject,可以在 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 和主项目依赖的两个项目:Dependency1ProjDependency2Proj

编译并运行解决方案——您将看到以下内容

窗口中垂直堆叠了五个不同颜色的方形按钮。

以下是 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 的行,BlueButtonMainWindow.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"  

GreenButtonGrayButton 定义在 Dependency2Proj 的两个不同命名空间中。GreenButton 定义在项目的主命名空间 Dependency2Proj 下,而 GrayButton 定义在 Dependency2Proj.SubFolder 命名空间下。然而,Dependency2Proj 也有一个 AssemblyInfo.cs 文件,其中定义了程序集元数据。在这个文件中,我们在底部添加了几行

[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj")]
[assembly: XmlnsDefinition("https://avaloniademos.com/xaml", "Dependency2Proj.SubFolder")]  

这两行将程序集的两个命名空间:Dependency2ProjDependency2Proj.SubFolder 合并到同一个 URL:“https://avaloniademos.com/xaml”。如上所述,该 URL 是否存在或计算机是否在线都无关紧要。如果您的 URL 能够传达与包含此功能的项目相对应的某种含义,那将是很好的。

现在,我们用来引用 GreenButtonGrayButton 的 XAML 前缀 dep2 是通过引用该 URL 定义的

xmlns:dep2="https://avaloniademos.com/xaml"  

与 WPF 相比,Avalonia 有一个重要的额外功能。在 WPF 中,只能通过 URL 引用不位于与要引用它的 XAML 文件相同的项目中的功能,而在 Avalonia 中,没有这种限制——例如,如果我们在同一个 Dependency2Proj 项目中有一个 XAML 文件,我们仍然可以添加一行...

xmlns:dep2="https://avaloniademos.com/xaml" 

...在其顶部元素处,并通过 dep2: 前缀引用在同一项目中定义的 GreenButtonGrayButton

在 XAML 中访问 C# 复合属性

我们前面已经提到,C# 内置属性可以作为相应元素的 XML 属性访问,例如

<my_namespace:MyClass Prop1="Hello World"
                      Prop2="123"/>

Prop1Prop2 是在 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>  

请注意我们将 WindowContent 属性分配给复合 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:Namex: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 可以被 StaticResourceDynamicResource 用来引用特定的资源。

然后,我们使用 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 占用更多的内存资源。因此,当您不需要更改通知时(属性在程序运行期间保持不变),应始终使用 StaticResourceDynamicResources 在您想要动态更改应用程序主题或颜色时非常有用,例如,允许用户切换主题或根据一天中的时间更改颜色。

引用不同 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 定义了两个值:泛型类型 TVal1Value1 和泛型类型 TVal2Value2

其余有趣的代码都位于 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 对象定义为包含示例代码的 GridDataContext

<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 的所有后代上定义。我们可以将后代 TextBlocksText 属性绑定到 Value1Value2 以显示这些值。此外,为了证明 Value1 确实是 double 类型(而不是 string),我们将内部网格(带黄色背景)的宽度绑定到 DataContextValue1 属性

 <Grid Background="Yellow"
      ...
      Width ="{Binding Path=Value1}" ...>   

这样黄色矩形的宽度将为 300。

ValuesContainer 对象列表示例

在查看示例的 XAML 代码之前,请注意我们在 XAML 文件顶部定义了一个命名空间 collectionsxmlns: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

然后我们将 ItemsControlItems 属性绑定到列表,并使用 ItemsControlItemTemplate 来显示每个单独项目的 Value1Value2

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

第一个 TextBlockText 属性绑定到 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日:文章更新
© . All rights reserved.