简单示例中的多平台Avalonia .NET Framework编程基本概念

目录

介绍

关于Avalonia

本文的目的

本文的组织

示例代码

解释概念

视觉树

Avalonia 工具

逻辑树

附加属性

样式属性

直接属性

有关附加、样式和直接属性的更多信息

绑定

什么是Avalonia UI和WPF中的绑定以及为什么需要它

关于Avalonia绑定的好处

Avalonia绑定概念

在XAML中演示不同的绑定源

DataContext(默认)绑定源

设置Binding.Source属性

按ElementName绑定

使用RelativeSource绑定到自身

绑定到TemplatedParent

使用带AncestorType的RelativeSource绑定到视觉树祖先

使用具有AncestorType和AncestorLevel的RelativeSource绑定到视觉树祖先

使用Avalonia绑定路径简写在逻辑树中查找父级

使用Avalonia绑定路径简写在逻辑树中查找Grid类型的第一个父级

使用Avalonia绑定路径简写绑定到逻辑树中的第二个祖先网格

演示不同的绑定模式

绑定转换器

多值绑定示例

在C#代码中创建绑定

绑定到非可视类的属性

结论


介绍

本文可视为在Easy Samples中使用AvaloniaUI进行多平台UI编码——第1部分——AvaloniaUI构建块,即使您不必阅读第一篇文章即可理解本文的内容。

关于Avalonia

Avalonia是一个新的开源包,它与WPF非常相似,但与WPFUWP不同,它适用于大多数平台——WindowsMacOS 和各种Linux版本,并且在许多方面都比WPF更强大。

avalonia的源代码可在GitHub上Avalonia源代码上找到。

有一些可用的Avalonia文档虽然并不广泛,但应该会快速改进。

AvaloniaGitter上有一个不错的免费公共支持:Gitter 上的Avalonia以及在Avalonia Support 购买商业支持的一些选项 你也可以在Avalonia Github Discussions中提问。

Avalonia是比Web编程框架或Xamarin更好的框架的原因在上一篇文章中有详细描述:在Easy Samples中使用AvaloniaUI进行多平台UI编码——第1部分——AvaloniaUI构建块。在这里,我只总结两个主要原因:

  1. Avalonia框架(就像WPF一样)是100%组合的——简单的按钮可以由几何路径、边框和图像等基元组合而成,就像可以创建非常复杂的页面或视图一样。开发人员可以选择控件的外观和行为方式以及可自定义的属性。此外,更简单的基元可以组织成更复杂的基元,从而降低复杂性。HTML/JavaScript/TypeScript框架和Xamarin的组合程度都不同——事实上,它们的基元是按钮、复选框和菜单,它们带有许多要修改以进行自定义的属性(某些属性可以特定于平台或浏览器)。在这方面,Avalonia开发人员有更多的自由来创建客户需要的任何控件。
  2. WPF提出了许多新的开发范例,可以帮助更快、更清晰地开发可视化应用程序——其中包括可视化和逻辑树、绑定、附加属性、附加路由事件、数据和控制模板、样式、行为。这些范式中很少有在Web框架和Xamarin中实现,并且它们在那里的功能要弱得多,而在Avalonia中——所有这些范式都已实现,并且某些(例如,属性和绑定)甚至以比WPF更强大的方式实现。

本文的目的

本文的主要目的是为那些不一定了解WPF的人解释最重要的Avalonia/WPF概念。对于WPF专家,本文将作为通往Avalonia的门户。

我试图通过提供解释、详细图片和简单的Avalonia示例来阐明这些概念,尽可能突出这些概念。

本文的组织

本文将涵盖以下主题:

  1. 视觉树
  2. 逻辑树
  3. 附加属性
  4. 样式属性
  5. 直接属性
  6. 绑定

以下主题将留给以后的文章:

  1. RoutedEvents
  2. Commands
  3. ControlTemplates(基本的)
  4. MVVM模式DataTemplatesItemsPresenterContentPresenter
  5. 从XAML调用C#方法
  6. XAML——通过标记扩展重用Avalonia XAML
  7. 样式、转换、动画

示例代码

示例代码位于Avalonia概念文章的演示代码下。这里的所有示例都在Windows 10MacOS CatalinaUbuntu 20.4上进行了测试

所有代码都应该在Visual Studio 2019下编译和运行——这就是我一直在使用的。此外,请确保在第一次编译示例时您的Internet连接已打开,因为必须下载一些nuget包。

解释概念

视觉树

Avalonia(和WPF)基本构建块(基元)包括:

  1. 基元元素——在Avalonia宇宙中无法分解为子元素的非常基本的元素,如TextBlockBorderPathImage, Viewbox等。
  2. 面板——负责在其中安排其他元素的元素。

其余控件(更复杂的控件,包括诸如ButtonComboBoxMenu等的基本控件)和复杂视图是通过将各种基元放在一起并将它们放置在其他基元或面板中来构建的。在Avalonia中,基元通常从Control类继承,而更复杂的控件从TemplatedControl类继承,而在WPF中,基元继承自Visual,更复杂的控件继承自Control(在WPF中,ControlTemplate属性和相关的基础设施,而在Avalonia中,是TemplatedControl拥有它们)。您可以在上一篇文章的Avalonia Primitives部分阅读更多关于Avalonia基元的信息。

Avalonia(和WPF)视觉对象的组合可以是分层的:我们从基元中创建一些更简单的对象,然后从那些更简单的对象(可能还有基元)中创建更复杂的对象,等等。这种分层组合的原则是核心方式之一或重用视觉组件。

下图显示了一个简单的按钮可能由几个原始元素组成:例如,它可能由一个Grid面板组成,该面板具有一个按钮文本TextBlock对象和一个按钮图标Image对象。对象的这种包含结构清楚地定义了一个简单的树——视觉树。

这是上面描述的一个非常简单的按钮的图形:

这是按钮的视觉树图:

当然,真正的按钮的视觉树可能更复杂,还包括按钮的边框和阴影的边框以及一个或多个覆盖面板,一旦鼠标悬停在按钮上就会改变不透明度或颜色,以指示该按钮在鼠标单击时处于活动状态,并且其他很多,不过为了解释可视化树的概念,上面描述的按钮就可以了。

现在启动NP.Demos.VisualTreeSample.sln解决方案。此解决方案中与默认内​​容不同的唯一文件是MainWindow.axaml.axaml文件与仅由Avalonia使用的.xaml文件非常相似,以便它们与WPF .xaml文件共存)和MainWindow.axaml.cs您可以在在Easy Samples中使用AvaloniaUI进行多平台UI编码——第1部分——AvaloniaUI构建块找到有关AvaloniaUI应用程序项目中文件的更多信息。使用Visual Studio 2019创建和运行简单的AvaloniaUI项目部分

这是MainWindow.xaml的内容:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="NP.Demos.VisualAndLogicalTreeSample.MainWindow"
        Title="NP.Demos.VisualAndLogicalTreeSample"
        Width="300"
        Height="200">
  <Button x:Name="SimpleButton"
          Content="Click Me" 
          HorizontalAlignment="Center"
          VerticalAlignment="Center"/>
</Window>  

还请看一下App.axaml文件。您将看到对以下FluentTheme内容的引用:

<Application.Styles>
    <FluentTheme Mode="Light"/>
</Application.Styles>  

主题定义了所有主要控件的外观和行为,当然包括按钮。他们通过使用样式和模板来做到这一点,具体如何——稍后将在这些系列文章中进行解释。重要的是要理解,我们的Button视觉树是由ButtonControlTemplate定义的,它位于Button样式中,而按钮的样式又位于FluentTheme中。

这是您在运行项目时看到的内容:

单击窗口以获得鼠标焦点,然后按F12键。Avalonia工具窗口将打开:

工具窗口类似于WPF snoop(尽管它在某些方面仍然不如WPF snoop强大)。它使您能够调查视觉树或逻辑树中任何元素的任何属性或事件。

Avalonia中,逻辑树(稍后将提供它的解释)比WPF中发挥更大的作用,因此默认情况下,该工具显示逻辑树,为了切换到可视树,您需要单击视觉树选项卡(在上图中由读取的椭圆突出显示)。

一旦我们切换工具以显示可视树,同时按下ControlShift键并将鼠标放在按钮的文本上。工具左侧的可视化树将扩展为包含Button的文本的元素,工具中间的属性窗格将显示可视化树的当前选定元素的属性(在我们的示例中为ButtonTextBlock元素):

可视化树实际上是针对整个窗口显示的(其中与当前选定元素对应的部分被展开)。

您可以看到来自FluentThemeButtonVisual Tree实际上比我们上面考虑的更简单——它仅包含三个元素——Button(根元素),然后是ContentPresenter并且然后是TextBlock元素:

你可以选择一个不同的元素,比如Button——它是TextBlock的父元素,在工具的中间窗格中查看button的属性。如果您正在查找特定属性,例如DataContext,您可以在属性表顶部键入其名称的一部分,例如context,它会将属性过滤到名称中包含单词context的属性:

有关该工具的更多信息将在下一节中介绍。

用于获取Visual Tree节点的C#功能示例位于OnButtonClick方法内的MainWindow.xaml.cs文件中:

private void OnButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
    IVisual parent = _button.GetVisualParent();

    var visualAncestors = _button.GetVisualAncestors().ToList();

    var visualChildren = _button.GetVisualChildren().ToList();

    var visualDescendants = _button.GetVisualDescendants().ToList();
}  

附加此方法来处理按钮的单击事件:

_button = this.FindControl<Button>("SimpleButton");

_button.Click += OnButtonClick; 

请注意,为了使Visual Tree扩展方法可用,我们必须在MainWindow.axaml.cs文件的顶部添加using Avalonia.VisualTree;命名空间引用。

在方法的最后放置一个断点,然后单击按钮。您可以在Watch窗口中调查OnButtonClick()方法内变量的内容:

可以看到结果与Tool中观察到的Visual Tree一致:

的确,

  1. 我们的Button父母是ContentPresenter
  2. 我们Button有四个祖先:ContentPresenterVisualLayoutManager,PanelWindow,
  3. 我们Button只有一个孩子——一个ContentPresenter
  4. 我们Button有两个后代:ContentPresenter和 TextBlock

Avalonia 工具

一旦我们在上一节中提到了Avalonia工具,让我们在这里提供更多关于它的信息。

该工具的美妙之处在于它也是用Avalonia编写的,因此它是多平台的。如果您想检查这些平台上的树和属性,它也会在MacOSLinux上显示——您需要做的就是单击您希望该工具工作的窗口并按F12键。

该工具仅显示与单个窗口对应的信息,因此如果您使用多个窗口,您要调查其树和属性,则必须使用多个工具窗口。

对于没有DEBUG设置预处理变量的配置,例如默认发布配置,该工具不会显示。事实上,MainWindow构造函数(位于MainWindow.axaml.cs文件中)中的以下行创建了启动工具的能力:

#if DEBUG
            this.AttachDevTools();
#endif  

此外,如果您不需要该工具,一旦您删除对this.AttachDevTool()的调用,您也可以从您的引用中删除Avalonia.Diagnostics包。

逻辑树

逻辑树是可视树的一个子集——它比可视树更稀疏——其中的元素更少。它紧跟XAML代码,但不扩展任何控件模板(它们是什么将在以后的文章中解释)。当显示一个 ContentControl时,它直接从ContentControl表示它的元素Content(省略其间的所有内容)。当显示ItemsControl时,它会直接从ItemsControl元素到表示其项目内容的元素,同时省略其间的所有内容。

该代码位于NP.Demos.LogicalTreeSample.sln解决方案下。这是您运行后将看到的内容:

这是从MainWindow.xaml文件生成此布局的XAML代码:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="NP.Demos.LogicalTreeSample.MainWindow"
        Title="NP.Demos.LogicalTreeSample"
        Width="300"
        Height="200">

  <Grid RowDefinitions="*, *">
    <Button x:Name="ClickMeButton" 
            Content="Click Me"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"/>

    <ItemsControl Grid.Row="1"
                  HorizontalAlignment="Center"
                  VerticalAlignment="Center">
      <Button x:Name="Item1Button" 
              Content="Item1 Button"/>
      <Button x:Name="Item2Button"
              Content="Item2 Button"/>
    </ItemsControl>
  </Grid>
</Window>  

我们看到窗口的内容由两行的Grid面板表示。顶行包含Button Click Me,底行包含ItemsControl两个按钮:Item1 ButtonItem2 Button。按钮的XAML名称与其中写入的名称相同,只是没有空格:ClickMeButtonItem1ButtonItem2Button

单击示例窗口,然后按F12启动工具并展开工具内的逻辑树——您将看到以下内容:

您可以看到,只有与MainWindow.axaml文件的XAML标记相对应的元素出现在可视化树中以及TextBox中的Buttons(因为按钮是 ContentControl——TextBox是显示其内容的元素)。许多将出现在Visual Tree中的节点在这里都丢失了——你不会在这里找到由于控件模板的扩展而创建的Visual Tree元素——没有Window的边框、面板、VisualLayoutManager等,我们从Window跳过直接到Grid因为Grid元素是MainWindow.asaml文件的一部分。同样,我们从Button直接跳到TextBlock忽略了ContentPresenter,因为它来自Button的模板扩展。

现在看看MainWindow.axaml.cs文件中的OnButtonClick方法:

private void OnButtonClick(object? sender, RoutedEventArgs e)
{
    ItemsControl itemsControl = this.FindControl<ItemsControl>("TheItemsControl");

    var logicalParent = itemsControl.GetLogicalParent();
    var logicalAncestors = itemsControl.GetLogicalAncestors().ToList();
    var logicalChildren = itemsControl.GetLogicalChildren().ToList();
    var logicalDescendants = itemsControl.GetLogicalDescendants().ToList();
}  

我们正在获取ItemsControl元素的逻辑父、祖先、子孙和后代。此方法设置为ClickMeButtonClick事件处理程序:

Button clickMeButton = this.FindControl<Button>("ClickMeButton");
clickMeButton.Click += OnButtonClick;  

请注意,为了获得这些扩展方法,我们必须在MainWindow.xaml.cs文件的顶部添加using Avalonia.LogicalTree命名空间。

在方法的最后放置一个断点,运行应用程序并按下ClickMeButton。检查Watch窗口中的变量:

这与我们在工具中看到的完全对应。

附加属性

附加属性是一个非常重要且有用的概念,需要理解。它最初是由WPF引入的,并从那里直接进入了Avalonia,尽管它是一个更好和扩展的版本。

为了解释附加属性是什么,让我们首先记住什么是C#中的简单读/写属性。本质上,MyClass类中定义的类型T属性可以由两种方法表示——gettersetter方法:

public class MyClass  
{
  T Getter();
  void Setter(T value);
}

通常,此类属性由在同一类中定义的类型T的支持字段实现:

public class MyClass  
{
  // the backing field
  T _val;

  T Getter() => _val;
  void Setter(T value) => _val = value;
} 

WPF工作期间,WPF架构师面临一个有趣的问题。每个视觉对象都必须定义数百个(如果不是数千个)属性,其中大多数属性每次都有默认值。为每个对象中的每个属性定义一个支持字段将导致大量内存消耗,尤其是不必要的,因为每次这些属性中约有90%将具有默认值。

所以,为了解决这个问题,他们想出了附加属性。附加属性不是将属性值存储在对象内的支持字段中,而是将值存储在一种static hashtableDictionary(或Map)中,其中值由可能具有这些属性的各种对象索引。只有具有非默认属性值的对象在hashtable中,如果对象的条目不在hashtable中,则假定该对象的属性具有默认值。附加属性的静态哈希表实际上可以在任何类中定义——通常,它是在与使用其值的类不同的类中定义的。所以非常粗略(和近似)地说——附加属性MyAttachedProperty的实现,类型为double,在类MyClass上的实现类似于:

public class MyClass
{

}

public static class MyAttachedPropertyContainer
{
    // Attached Property's default value
    private static double MyAttachedPropertyDefaultValue = 5.0;

    // Attached Property's Dictionary
    private static Dictionary<MyClass, double> MyAttachedPropertyDictionary =
                                              new Dictionary<MyClass, double>();

    // property getter
    public static double GetMyAttachedProperty(this MyClass obj)
    {
        if (MyAttachedPropertyDictionary.TryGetValue(obj, out double value)
        {
            return value;
        }
        else // there is no entry in the Dictionary for the object
        {
            return MyAttachedPropertyDefaultValue; // return default value
        }
    }

    // property setter
    public static SetMyAttachedProperty(this MyClass obj, double value)
    {
        if (value == MyAttachedPropertyDefaultValue)
        {
           // since the property value on this object 'obj' should become default,
           // we remove this object's entry from the Dictionary -
           // once it is not found in the Dictionary, - the default value will be returned
           MyAttachedPropertyDictionary.Remove(obj);
        }
        else
        {
            // we set the object 'to have' the passed property value
            // by setting the Dictionary cell corresponding to the object
            // to contain that value
            MyAttachedPropertyDictionary[obj] = value;
        }
    }
}

因此,不是每个类型MyClass的对象都包含该值,而是该值位于由该类型MyClass的对象索引的一些static Dictionary对象中。还可以为属性指定一些默认值(在我们的例子中,它是5.0),这样只有具有非默认属性值的对象才需要在Dictionary中的实体。

这种方法节省了大量内存,但代价是属性的gettersetter稍慢。

一旦尝试了附加属性,就会发现除了节省内存之外,它们还提供了许多其他好处——例如:

  • 您可以轻松地向它们添加一些属性更改通知回调,这些回调将在对象的属性更改时触发。
  • 您可以在类上定义附加属性,而无需修改类本身。这是极其重要的。一个明显的例子——通常的按钮没有CornerRadius属性。假设您的应用程序中有许多不同类型的按钮,突然间,用户要求它们中的许多应该有一些平滑的边框角,除了不同的按钮应该有不同的角半径。您不想为按钮创建新的派生类型并在任何地方替换它们并重新测试它们中的每一个,但是您可以稍微修改按钮的样式。您可以创建附加属性TheCornerRadiusProperty,绑定按钮TheCornerRadiusProperty的按钮边框CornerRadius属性,并将此属性设置为各个按钮样式中所需的值。
  • 概括上一项,附加属性允许创建和附加行为到视觉对象——行为是允许修改和增强视觉对象的功能而不修改视觉对象的类的复杂类。行为对于本文来说有点过于复杂,将在未来进行描述。

当然,上面显示的非常简单的实现,并没有考虑很多其他问题,如线程、回调、注册(以便了解我们类MyClass上允许的所有附加属性)等等。此外,像我们上面所做的那样,将默认值本身定义为属性之外的static变量是很难看的。由于这些考虑,创建一个特殊类型AttachedProperty<...>(可能带有一些通用参数)是有意义的,它将包含Dictionary、默认值和属性运行所需的许多其他功能。这就是WPFAvalonia所做的。

在我们继续Attached Property示例之前,最好下载我在Avalonia Snippets上提供的Avalonia片段并安装它们。可以在相同的URL中找到安装说明。

附加属性示例位于NP.Demos.AttachedPropertySample.sln解决方案下。尝试运行它。这是您将看到的内容:

滑块的变化可以在值010之间变化,并且当您更改滑块位置时,矩形的StrokeThickness属性会相应更改——矩形变得更厚或更薄(当滑块位置为0时,矩形完全消失)。

查看AttachedProperties.cs文件的内容——RectangleStrokeThickness附加属性在此处定义。这个属性是使用avap片段创建的(它的名字代表Avalonia附加属性):

public static class AttachedProperties
{
    #region RectangleStrokeThickness Attached Avalonia Property

    // Attached Property Getter
    public static double GetRectangleStrokeThickness(AvaloniaObject obj)
    {
        return obj.GetValue(RectangleStrokeThicknessProperty);
    }

    // Attached Property Setter
    public static void SetRectangleStrokeThickness(AvaloniaObject obj, double value)
    {
        obj.SetValue(RectangleStrokeThicknessProperty, value);
    }

    // Static field that of AttachedProperty<double> type. This field contains the
    // Attached Properties' Dictionary, the default value and the rest of the required 
    // functionality
    public static readonly AttachedProperty<double> RectangleStrokeThicknessProperty =
        AvaloniaProperty.RegisterAttached<object, Control, double>
        (
            "RectangleStrokeThickness", // property name
            3.0 // property default value
        );

    #endregion RectangleStrokeThickness Attached Avalonia Property
}  

我们可以看到:

  • public static double GetRectangleStrokeThickness(AvaloniaObject obj)是getter(类似于上面讨论的那个),
  • public static void SetRectangleStrokeThickness(AvaloniaObject obj, double value)是setter。
  • public static readonly AttachedProperty<double> RectangleStrokeThicknessProperty</double>是包含Dictionary(或对象到值hashtable)和附加属性的默认值以及所有其余所需功能的static字段。

定义附加属性所需的代码量看起来很庞大,但它们都是在avap代码段的帮助下在几秒钟内创建的。因此,如果您计划使用Avalonia——片段是必须的(与WPF中相同)。您还可以看到我的代码片段将每个附加属性放在自己的区域中,以便可以折叠它并使代码更具可读性。

现在,看看MainWindow.cs文件中的XAML代码:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:NP.Demos.AttachedPropertySample"
        x:Class="NP.Demos.AttachedPropertySample.MainWindow"
        Title="NP.Demos.AttachedPropertySample"
        local:AttachedProperties.RectangleStrokeThickness="7"
        Width="300"
        Height="300">
  <Grid RowDefinitions="*, Auto">
        <Rectangle Width="100"
                   Height="100"
                   Stroke="Green"
                   StrokeThickness="{Binding Path=
                                    (local:AttachedProperties.RectangleStrokeThickness), 
                                     RelativeSource={RelativeSource AncestorType=Window}}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"/>
    
      <Slider Minimum="0"
              Maximum="10"
              Grid.Row="1"
              Value="{Binding Path=(local:AttachedProperties.RectangleStrokeThickness), 
                              Mode=TwoWay, 
                              RelativeSource={RelativeSource AncestorType=Window}}"
              Margin="10,20"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Width="150"/>
  </Grid>
</Window>  

请注意,在XAML中,我使用的是绑定——一个非常重要的概念,稍后将更详细地解释。

我们有一个Grid有两行的面板——顶行有一个Rectangle,底部有一个Slider控件。Slider可以在010之间更改其值。

几乎在最顶端——Window XAML标记中有以下行:

xmlns:local="clr-namespace:NP.Demos.AttachedPropertySample"  

这一行定义了本地XAML命名空间,以便通过该命名空间,我们可以引用我们的RectangleStrokeThickness附加属性。

下一个有趣的行是:

local:AttachedProperties.RectangleStrokeThickness="7"  

在这里,我们将窗口对象上的RectangleStrokeThickness附加属性的初始值设置为数字7。请注意我们如何指定附加属性<namespace-name>:<class-name>.<AttachedProperty-name>

代码行...

StrokeThickness="{Binding Path=(local:AttachedProperties.RectangleStrokeThickness), 
                          RelativeSource={RelativeSource AncestorType=Window}}"

... Rectangle标签下,将矩形的属性StrokeThickness绑定到矩形窗口祖先上的附加属性RectangleStrokeThickness请注意在Binding中的Attached Property格式——Attached Property全名在 paratheses内——这是AvaloniaWPF中的要求——如果没有paratheses,绑定将不起作用,人们可能会花费数小时试图找出问题所在。

Slider的代码行:

Value="{Binding Path=(local:AttachedProperties.RectangleStrokeThickness), 
                Mode=TwoWay, 
                RelativeSource={RelativeSource AncestorType=Window}}"  

SliderValue属性绑定到滑块的窗口祖先的RectangleStrokeThickness附加属性(当然,这是与Rectangle的窗口祖先相同的Window对象)。这个绑定是一个TwoWay绑定——意味着对SliderValue属性的更改也会更改Window上的RectangleStrokeThickness附加属性值。

这个视图的操作原理很简单——通过移动Slider的所谓的thumb来改变Slider的值——将触发Window上的RectangleStrokeThickness附加属性的变化(通过Slider的绑定),这反过来又会触发更改Rectangle上的StrokeThickness属性(通过其绑定)。

当然,在这个简单的例子中,我们可以直接将 SliderValue连接到RectangleStrokeThickness属性而不涉及Window上的Attached Property,但是该示例不会演示Attached Properties是如何工作的(在许多情况下,例如,当控件上不存在所需的属性附加属性是必须的)。

现在尝试删除顶部将初始值设置为7的行:

local:AttachedProperties.RectangleStrokeThickness="7"  

并重新启动应用程序。你会看到RectangleStrokeThicknessSliderValue初始值变成了3.0而不是7.0。这是因为我们的附加属性的默认值是3.0在注册附加属性时定义的。

现在让我们讨论附加属性更改通知。

查看文件MainWindow.axaml.cs:这是该文件中有趣的代码:

public partial class MainWindow : Window
{
    // to stop change notification dispose of this subscription token
    private IDisposable _changeNotificationSubscriptionToken;

    public MainWindow()
    {
        InitializeComponent();

        ...

        // subscribe
        _changeNotificationSubscriptionToken =
            AttachedProperties
                .RectangleStrokeThicknessProperty
                .Changed
                .Subscribe(OnRectangleStrokeThicknessChanged);
    }

    // this method is called when the Attached property changes
    private void OnRectangleStrokeThicknessChanged
    (AvaloniaPropertyChangedEventArgs<double> changeParams)
    {
        // if the object on which this attached property changes
        // is not this very window, do not do anything
        if (changeParams.Sender != this)
        {
            return;
        }

        // check the old and new values of the attached property. 
        double oldValue = changeParams.OldValue.Value;

        double newValue = changeParams.NewValue.Value;
    }  

    ...
}

在顶部,我们定义了订阅令牌——它是IDisposable如果我们想停止对订阅更改做出反应,我们可以调用_changeNotificationSubscriptionToken.Dispose()

附加属性更改的订阅发生在构造函数中:

// subscribe
_changeNotificationSubscriptionToken =
    AttachedProperties
        .RectangleStrokeThicknessProperty
        .Changed
        .Subscribe(OnRectangleStrokeThicknessChanged);  

当值改变时调用该void OnRectangleStrokeThicknessChanged(...)方法。该方法接受一个类型AvaloniaPropertyChangedEventArgs<double>的参数,该参数包含所有必需的信息:

  1. 附加属性更改的对象由属性Sender提供
  2. OldValue属性携带有关先前值的信息。
  3. NewValue属性携带有关当前值的信息。

您可以在方法的末尾放置一个调试断点,在调试器上启动应用程序并尝试移动滑块——您将在断点处停止并能够调查当前值。

另一种更简单的方法(不能终止订阅)是在MainWindow.axaml.cs文件中创建一个static构造函数并使用AddClassHandler扩展方法:

static MainWindow()
{
    AttachedProperties
        .RectangleStrokeThicknessProperty
        .Changed
        .AddClassHandler<MainWindow>((x, e) => x.OnAttachedPropertyChanged(e));
}

private void OnAttachedPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
    double? oldValue = (double?) e.OldValue;

    double? newValue = (double?)e.NewValue;
}  

注意,这里不需要检查发送者是否与当前对象相同。

您可以看到该OnAttachedPropertyChanged(...)方法的类型安全签名稍差。通常,这种方式非常好,99%的时间,您都可以使用AddClassHandler(...)

您可能已经注意到,Avalonia在附加属性更改通知方面使用了强大的IObservable响应式扩展范例。

样式属性

WPF有一个依赖属性的概念,它与附加属性基本相同,只是它们定义在使用它们的同一个类中,相应地,它们的gettersetter放在同名的class属性中。请注意,使用依赖属性,我们仍然具有不会在默认值上浪费内存和轻松添加回调的优势,但是我们失去了在不修改类的情况下添加属性的优势。

我尝试在Avalonia中使用本地定义的附加属性,但我没有发现它们有任何问题,但根据Avalonia文档,最好使用所谓的样式属性(为什么——我现在不确定)。

我们将按照文档并运行一个示例来展示如何使用所谓的样式属性。

对于示例,打开NP.Demos.StylePropertySample.sln解决方案。

该示例将以与前一个完全相同的方式运行,并且代码非常相似,只是我们不使用在AttachedProperties.cs文件中定义的RectangleStrokeThickness附加属性,而是使用在MainWindow.axaml.cs文件中定义的同名的Style属性。您可以看到Style Propertygettersetter是非静态的,并且相当简单:

#region RectangleStrokeThickness Styled Avalonia Property
public double RectangleStrokeThickness
{
    // getter 
    get { return GetValue(RectangleStrokeThicknessProperty); }

    // setter
    set { SetValue(RectangleStrokeThicknessProperty, value); }
}

// the static field that contains the hashtable mapping the 
// object of type MainWindow into double and also containing the 
// information about the default value
public static readonly StyledProperty<double> RectangleStrokeThicknessProperty =
    AvaloniaProperty.Register<MainWindow, double>
    (
        nameof(RectangleStrokeThickness)
    );
#endregion RectangleStrokeThickness Styled Avalonia Property  

这个style属性也是使用我的其他代码片段在几秒钟内创建的——avsp代表Avalonia样式属性。

直接属性

有时,想要使用一个由字段支持的简单C#属性,但又能够订阅其更改并将该属性用作某些绑定的目标——是的,只有AttachedStyleDirect属性可以用作目标Avalonia绑定。简单的C#属性仍然可以用作绑定的来源,通过触发INotifyPropertyChanged接口的PropertyChanged事件来提供更改通知。

直接属性样本位于NP.Demos.DirectPropertySample.sln解决方案中。该演示的行为方式与前两个演示完全相同,只是我们使用的是直接属性而不是Style或附加属性。

下面是在MainWindow.xaml.cs文件中定义Direct属性的方式:

#region RectangleStrokeThickness Direct Avalonia Property
private double _RectangleStrokeThickness = default;

public static readonly DirectProperty<MainWindow, double> RectangleStrokeThicknessProperty =
    AvaloniaProperty.RegisterDirect<MainWindow, double>
    (
        nameof(RectangleStrokeThickness),
        o => o.RectangleStrokeThickness,
        (o, v) => o.RectangleStrokeThickness = v
    );

public double RectangleStrokeThickness
{
    get => _RectangleStrokeThickness;
    set
    {
        SetAndRaise(RectangleStrokeThicknessProperty, ref _RectangleStrokeThickness, value);
    }
}

#endregion RectangleStrokeThickness Direct Avalonia Property  

通过使用avdr片段(它的名称代表Avalonia Direct)在几秒钟内创建了此Direct Property

有关附加、样式和直接属性的更多信息

AttachedProperty<...>StyleProperty<...>DirectProperty<...>类都派生自AvaloniaProperty类。

如上所述,只有AttachedStyle Direct属性可以作为Avalonia UI绑定的目标。

AttachedStyleDirect属性只能在AvaloniaObject实现的类上设置——这是所有Avalonia视觉效果都实现的非常基本的类。

如果您不需要更改变量的先前值(在我们上面的OldValue示例中),订阅AttachedStyleDirect属性更改的最佳方法是使用该AvaloniaObject.GetObservable(AvaloniaProperty property)方法。

为了演示使用GetObservable(...)方法,我们可以修改我们的附加属性示例,如下所示:

public MainWindow()
{
...
_changeNotificationSubscriptionToken =
    this.GetObservable(AttachedProperties.RectangleStrokeThicknessProperty)
        .Subscribe(OnStrokeThicknessChanged);
}

private void OnStrokeThicknessChanged(double newValue)
{
...
}

您可以看到OldValue在回调中不再可用。

绑定

什么是Avalonia UIWPF中的绑定以及为什么需要它

绑定是一个非常强大的概念,它允许绑定两个属性,这样当其中一个属性发生变化时,另一个也会发生变化。通常,绑定从source属性到target属性——正常OneWay绑定,但也有一个TwoWay绑定可以确保两个属性同步,无论哪个发生变化。还有另外两种绑定模式:OneWayToSourceOneTime使用频率较低的绑定。

也很少讨论,但同样重要的集合绑定,其中一个集合模仿另一个集合,或者两个集合互相模仿。

请注意,绑定的目标不必与绑定的源完全相同,可以在源和目标之间进行转换,反之亦然,如下所示。

绑定是所谓的MVVM模式背后的主要概念(将在以后的一篇文章中详细讨论)。MVVM模式的核心思想是复杂的视觉对象模仿非常简单的非视觉对象的属性和行为——即所谓的视图模型(VM)

正因为如此,大部分业务逻辑可以在简单的非可视对象上开发和测试,然后通过绑定传输到非常复杂的可视对象,该对象将自动以类似的方式运行。

关于Avalonia绑定的好处

WPF绑定相比,Avalonia绑定更强大、更少错误和古怪且更易于使用——原因是它们是由非常聪明的人(或人)最近构建的,显然喜欢WPF绑定的Steven Kirk知道他们的怪癖和限制,此外还了解软件开发理论和实践的最新进展——反应式扩展。

关于Avalonia绑定的另一个好处是,与许多其他Avalonia功能不同,它们有很好的文档记录:在Avalonia Data Bindings Documentation

综上所述,我认为展示如何在真实的C#/XAML示例中创建各种绑定将很有用,特别是对于那些没有WPF经验的人。

Avalonia绑定概念

Avalonia Binding是一个复杂的对象,具有许多功能,其中一些最重要的功能,我将在本小节中讨论。

下图最好地解释了Avalonia(和WPF)绑定:

以下绑定部分很重要:

  1. 绑定源对象——通过该对象可以获得绑定源属性的路径。
  2. 绑定目标对象——其AvaloniaProperty(附加、样式或直接)属性用作绑定目标的对象。目标对象只能是派生自AvaloniaObject(这意味着它可以是任何Avalonia视觉对象)的类。AvaloniaObject类似于WPF的DependencyObject
  3. 绑定路径——从源对象到源属性的路径。Path由路径链接组成,每个链接都可以是常规(C#)属性或Avalonia属性。在XAML绑定中,Avalonia属性应放在括号中。下面是XAML中绑定路径的示例:MyProp1.(local:AttachedProperties.AttachedProperty1).MyProp2。此路径意味着在源对象上查找常规C# MyProp1属性,然后在第一个链接返回的对象中查找附加属性AttachedProperty1(在本地命名空间的AttachedProperties类中定义),然后在该附加属性值中查找常规C# MyProp2属性。
  4. Target属性——只能是Attached、Style或Direct Property类型之一。
  5. BindingMode可以:
    1. OneWay——从源头到目标
    2. TwoWay——当源或目标发生变化时,另一个也将得到更新。
    3. OneWayToSource——当目标更新时,源也会更新,但反之则不然。
    4. OneTime——仅在初始化期间从源同步目标一次。
    5. Default——依赖于目标属性的首选绑定模式。初始化Attached Style或Direct Property时,可以指定首选绑定模式,在这种情况下将使用该模式(在绑定本身内未指定BindingMode时)。
  6. 转换器——仅当源值和目标值不同时才需要。它用于将值从源转换为目标,反之亦然。对于通常的绑定,转换器应该实现IValueConverter接口。

AvaloniaWPF中还有一个所谓的MultiBindingMultiBinding假设有多个绑定源,但仍然是同一个绑定目标。多个源由一个特殊的转换器组合成一个目标,该转换器在多重绑定的情况下实现IMultiValueConverter

绑定的复杂部分之一是在AvaloniaWPF中都有多种方法可以指定源对象,但Avalonia有更多方法可以做到这一点。以下是指定源对象的各种方法的说明:

  1. 如果您根本不指定源对象——在这种情况下,默认源对象将由Binding目标的DataContext属性给出。除非显式更改(有一些例外),否则DataContext会自动沿可视树传播。
  2. 您可以通过将源分配给绑定的Source属性,在XAML中显式指定源。您可以直接在C#中分配它,也可以在XAML中使用StaticResource标记扩展。
  3. 有一个ElementName属性可用于按名称在同一XAML文件中查找源元素。
  4. 有一个RelativeSource属性可以根据其Mode属性打开几种更有趣的定位源对象的方法:
    1. 对于Mode==Self,源对象将与目标对象相同。
    2. Mode==TemplatedParent只能在某些Avalonia TemplatedControlControlTemplate中使用——其含义的解释将在下一部分中给出。在控件模板内的TemplatedParent意味着绑定的源是使用该模板的控件。
    3. Mode==FindAncestor表示将在可视树中向上搜索源对象。在这种模式下也应该AncestorType使用属性来指定要搜索的源对象的类型。如果未指定其他内容,则该类型的第一个对象将成为源对象。如果AncestorLevel也设置为某个正整数N,则它指定该第N个祖先对象将被作为绑定的源返回(默认情况下AncestorLevel == 1)。
      在Avalonia(但不是WPF)中,RelativeSourceTree属性可以(令人惊讶地)设置为TreeType.Logical(默认情况下是TreeType.Visual)。在这种情况下,将在逻辑树(更稀疏且不太复杂)上搜索祖先。

现在理论已经足够了,让我们做一些实际的例子。

XAML中演示不同的绑定源

此示例位于NP.Demos.BindingSourcesSample.sln解决方案中。此示例显示了在XAML中设置绑定源的各种可能方法。

这是您在运行示例后看到的内容:

现在让我们一一浏览各种示例(所有示例都位于MainWindow.axaml文件中)并解释生成它的XAML代码。

DataContext(默认)绑定源

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        ...
        DataContext="This is the Window's DataContext"
        ...>
        ...
        <Grid ...>
            <TextBlock Text="{Binding}"/>
        </Grid>
        ...
</Window>

当绑定中没有指定源时,Binding的源恢复为元素的DataContext属性。在我们的示例中,在Window上设置了DataContext,但由于它沿可视树传播(除非明确更改)——我们的TextBlock有相同的DataContext——这只是由我们的TextBlock显示的一个简单string

设置Binding.Source属性

在我们的第二个示例中,我们使用StaticResource标记扩展将绑定的源设置为字符串“This is the Window's resource”,该字符串定义为Window的资源。

<Window xmlns="https://github.com/avaloniaui"
        ...>
  <Window.Resources>
    <x:String x:Key="TheResource">This is the Window's resource</x:String>
  </Window.Resources>
  ...
        <TextBlock Text="{Binding Source={StaticResource TheResource}}"
                   .../>
  ...
</Window>  

ElementName绑定

我们的窗口有XAML名称——TheWindow,我们使用它来绑定到它的Tag: (Tag是在每个Avalonia Control上定义的属性,它可以包含任何对象。)

<Window ...
        Tag="This is the Window's Tag"
        x:Name="TheWindow"
        ...>
        ...
             <TextBlock Text="{Binding #TheWindow.Tag}"
                        .../>
        ...      
</Window>   

以上Text={Binding Path=Tag, ElementName=TheWindow}Avalonia的简写。

使用RelativeSource绑定到自身

这个示例展示了元素如何在Self模式下使用RelativeSource将自己作为Binding的源对象。

<TextBlock Text="{Binding Path=Tag, RelativeSource={RelativeSource Self}}"
         Tag="This is my own (TextBox'es) Tag"
         .../>  

绑定到TemplatedParent

使用TemplatedParent模式的RelativeSource只能在ControlTemplate内部使用,并且使用它意味着绑定引用在当前模板实现的控件上定义的属性(或路径):

<TemplatedControl Tag="This is Control's Tag"
                  ...>
    <TemplatedControl.Template>
        <ControlTemplate>
            <TextBlock Text="{Binding Path=Tag, 
                       RelativeSource={RelativeSource TemplatedParent}}"/>
        </ControlTemplate>
    </TemplatedControl.Template>
</TemplatedControl>  

上面的代码意味着我们绑定到由ControlTemplate实现的TemplatedControl上的Tag属性。

使用带AncestorTypeRelativeSource绑定到视觉树祖先

指定AncestorType将向Binding表示RelativeSource处于FindAncestor模式。

<Grid ...
    Tag="This is the first Grid ancestor tag"
    ...>
    <StackPanel>
        <TextBlock Text="{Binding Path=Tag, 
                   RelativeSource={RelativeSource AncestorType=Grid}}"/>
    </StackPanel>
</Grid>

使用具有AncestorTypeAncestorLevelRelativeSource绑定到视觉树祖先

使用AncestorLevel,您可以指定不需要所需类型的第一个祖先,而是第N个——其中N可以是任何正整数。

在下面的代码中,我们在元素的祖先中搜索第二个Grid

<Grid ...
      Tag="This is the second Grid ancestor tag">
    <StackPanel>
        <Grid Tag="This is the first Grid ancestor tag">
            <StackPanel>
                <TextBlock Text="{Binding Path=Tag, 
                 RelativeSource={RelativeSource AncestorType=Grid, AncestorLevel=2}}"/>
            </StackPanel>
        </Grid>
    </StackPanel>
</Grid>

使用Avalonia绑定路径简写在逻辑树中查找父级

<Grid Tag="This is the first Grid ancestor tag">
  <StackPanel Tag="This is the immediate ancestor tag">
      <TextBlock Text="{Binding $parent.Tag}"/>
  </StackPanel>
</Grid>  

请注意,$parent.Tag意味着找到元素的父级(第一个祖先)并从中获取Tag属性。此绑定应等效于更长的版本:

<TextBlock Text="{Binding Path=Tag,
 RelativeSource={RelativeSource Mode=FindAncestor, Tree=Logical}}">

使用Avalonia绑定路径简写在逻辑树中查找Grid类型的第一个父级

<Grid Tag="This is the first Grid ancestor tag">
  <StackPanel Tag="this is the immediate ancestor tag">
    <Button Tag="This is the first logical tree ancestor tag">
      <TextBlock Text="{Binding $parent[Grid].Tag}"/>
    </Button>
  </StackPanel>
</Grid>  

$parent[Grid].Tag成功了。

使用Avalonia绑定路径简写绑定到逻辑树中的第二个祖先网格

<Grid Tag="This is the second Grid ancestor tag">
  <StackPanel>
    <Grid Tag="This is the first Grid ancestor tag">
      <StackPanel Tag="this is the immediate ancestor tag">
        <Button Tag="This is the first logical tree ancestor tag">
          <TextBlock Text="{Binding $parent[Grid;1].Tag}"/>
        </Button>
      </StackPanel>
    </Grid>
  </StackPanel>
</Grid>  

$parent[Grid;1]引用类型Grid的第二个祖先。这里有一个不一致的地方——祖先的编号在视觉树中从1开始,但在逻辑树中从0开始。

演示不同的绑定模式

此示例位于NP.Demos.BindingModesSample.sln解决方案下。此示例的所有代码都位于MainWindow.asaml文件中。

运行示例,您将看到以下内容:

前三个TextBoxes绑定到相同的WindowTag属性——第一个使用TwoWay模式,第二个——OneWay和第三个—— OneTime。尝试在顶部TextBox输入。然后,顶部的第二个TextBox将得到更新,但不是第三个:

这是可以理解的,因为顶部TextBox有一个使用Window's标签的TwoWay绑定——当你修改它的文本时,Window的标签也会被更新,并且绑定到同一个标签的一种方式将更新第二个TextBox

如果您尝试在第二个TextBox中修改文本,则不会发生任何事情,因为它具有OneWay——从WindowTagTextBox.Text的绑定。当然,当有人修改第三个TextBox中的文本时,什么都不会发生。 

这是前三个文本框的相关代码(第第四个是特殊的,我会解释——为什么——稍后)。

<Window Tag="Hello World!"
        ...>
    ...
    <TextBox ...
             Text="{Binding $parent[Window].Tag, Mode=TwoWay}"/>
    <TextBox ...
             Text="{Binding $parent[Window].Tag, Mode=OneWay}"/>
    <TextBox ...
             Text="{Binding $parent[Window].Tag, Mode=OneTime}"/>
    ...
</Window>

第四是TextBox示范OneWayToSource模式。请注意,最初,它不显示任何内容。如果你开始输入它,你会看到下面出现了相同的文本:

这是第四个TextBox的相关代码:

<Grid ...
      Tag="This is a OneWayToSource Grid Tag">
  ...
  <TextBox Text="{Binding $parent[Grid].Tag, Mode=OneWayToSource}"
           .../>
  <TextBlock Text="{Binding $parent[Grid].Tag, Mode=OneWay}"
             .../>
</Grid>  

TextBoxTextBlock都绑定到Grid panel上的Tag

请注意,Tag最初有一些文本:这是一个OneWayToSource网格标签。然而,TextBoxTextBlock一开始都是空的。这是因为OneWayToSource绑定删除了标签的初始值(TextBox最初没有任何文本在其中,因此它覆盖了绑定的Tag初始值)。

这就是我没有在第四个TextBox中使用Window Tag的原因——它会破坏其他三个TextBoxes的初始值。

这也是我很少使用OneWayToSource绑定的原因——如果它从Source分配初始值给Target,并且只有这样才会从Target工作到Source,那么它会有用得多。

绑定转换器

打开NP.Demos.BindingConvertersSample.sln解决方案。这是您运行后将看到的内容:

尝试从顶部TextBox删除文本。绿色文本将消失,而将出现红色文本:

此外,无论您在顶部或底部TextBox键入什么,相同的字符但从右到左倒置将出现在另一个TextBox中。

以下是相关代码:

<Grid ...>
    ...
  <TextBox  x:Name="TheTextBox" 
            Text="Hello World!"
            .../>
  <TextBlock Text="This text shows when the text in the TextBox is empty"
             IsVisible="{Binding #TheTextBox.Text, 
             Converter={x:Static StringConverters.IsNullOrEmpty}}"
             Foreground="Red"
             .../>
  <TextBlock Text="This text shows when the text in the TextBox is NOT empty"
             IsVisible="{Binding #TheTextBox.Text, 
             Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
             Foreground="Green"
             .../>
  <TextBox  Grid.Row="4"
            Text="{Binding #TheTextBox.Text, Mode=TwoWay, 
            Converter={StaticResource TheReverseConverter}}"
            ...>
</Grid>

对于这两个TextBlocks,我使用的是Avalonia内置转换器——IsNullOrEmptyIsNotNullOrEmpty。它们被定义为StringConverters类中的static属性,该类是默认Avalonia命名空间的一部分。这就是为什么不需要命名空间前缀的原因,这就是我使用x:Static标记扩展来查找它们的原因,例如Converter={x:Static StringConverters.IsNullOrEmpty}.

底部的TextBox使用在同一个项目中的ReverseStringConverter定义:

public class ReverseStringConverter : IValueConverter
{
    private static string? ReverseStr(object value)
    {
        if (value is string str)
        {
            return new string(str.Reverse().ToArray());
        }

        return null;
    }

    public object? Convert
    (object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ReverseStr(value);
    }

    public object? ConvertBack
    (object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ReverseStr(value);
    }
}  

请注意,转换器实现IValueConverter接口。它通过Convert(...)ConvertBack(...)方法相应地定义了前向和后向转换。底部的TextBox绑定当然是'TwoWay所以无论哪个TextBox改变,另一个也会改变。

多值绑定示例

下一个示例展示了如何将绑定的目标连接到多个源。该代码位于NP.Demos.MultiBindingSample.sln解决方案下。

运行示例,您将看到以下内容:

尝试在任何一个TextBox中输入smth。它们的串联将继续显示在底部。

这是执行此操作的相关代码:

<Grid RowDefinitions="Auto,Auto,Auto"
      <TextBox x:Name="Str1"
               Text="Hi"
               .../>
      <TextBox x:Name="Str2" 
               Text="Hello"
               .../>
      <TextBlock ...>
          <TextBlock.Text>
              <MultiBinding Converter="{x:Static local:ConcatenationConverter.Instance}">
                  <Binding Path="#Str1.Text"/>
                  <Binding Path="#Str2.Text"/>
              </MultiBinding>
          </TextBlock.Text>
      </TextBlock>
</Grid>  

MultiBinding包含两个到单个文本框的单值绑定:

<Binding Path="#Str1.Text"/>
<Binding Path="#Str2.Text"/>

它们的值由MultiValue转换器(Converter="{x:Static local:ConcatenationConverter.Instance}")转换为它们的串联。

MultiValue转换器在示例项目中的ConcatenationConverter类中定义:

public class ConcatenationConverter : IMultiValueConverter
{
    // static instance to reference
    public static ConcatenationConverter Instance { get; } =
        new ConcatenationConverter();

    public object? Convert(IList<object> values, 
           Type targetType, object parameter, CultureInfo culture)
    {
        if (values == null || values.Count == 0)
        {
            return null;
        }

        return 
            string.Join("", values.Select(v => v?.ToString()).Where(v => v != null));
    }
}  

该类实现了IMultiValueConverter接口(不是IValueConverter用于单值绑定转换)。

IMultiValueConverter只有一个方法——Convert(...)用于前向转换,它的第一个参数是IList<object>,其每个源值都有一个条目。

为了避免通过创建XAML资源来污染XAML代码,我创建了一个名Instance为的static属性,该属性引用同一类的全局实例,并且可以通过x:Static标记扩展从XAML轻松访问:Converter="{x:Static local:ConcatenationConverter.Instance}"

C#代码中创建绑定

下一个示例位于NP.Demos.BindingInCode.sln解决方案下。这是您运行后将看到的内容:

尝试更改中的文本——在您按下按钮绑定TextBox之前不会发生其他任何事情。按下它后,文本将出现在模仿其中的文本TextBox下方:

当您按下按钮取消绑定时,下面的文本将再次停止对修改做出反应。

此功能主要由MainWindow.asaml.cs中的代码实现。XAML代码简单地定义了TextBoxTextBlock,并将它们放在它下面,以及两个按钮: BindButtonUnbindButton

...
<StackPanel ...>
    <TextBox x:Name="TheTextBox"
             Text="Hello World"/>
    <TextBlock x:Name="TheTextBlock"
               HorizontalAlignment="Left"/>
</StackPanel>
...
<StackPanel ...>
    <Button x:Name="BindButton" 
            Content="Bind"/>

    <Button x:Name="UnbindButton"
            Content="Unbind"/>
</StackPanel>
...  

这是相关的C#代码:

public partial class MainWindow : Window
{
    TextBox _textBox;
    TextBlock _textBlock;
    public MainWindow()
    {
        InitializeComponent();
        ...
        _textBox = this.FindControl<TextBox>("TheTextBox");
        _textBlock = this.FindControl<TextBlock>("TheTextBlock");

        Button bindButton = this.FindControl<Button>("BindButton");
        bindButton.Click += BindButton_Click;

        Button unbindButton = this.FindControl<Button>("UnbindButton");
        unbindButton.Click += UnbindButton_Click;
    }

    IDisposable? _bindingSubscription;
    private void BindButton_Click(object? sender, RoutedEventArgs e)
    {
        if (_bindingSubscription == null)
        {
            _bindingSubscription =
                _textBlock.Bind(TextBlock.TextProperty, 
                                new Binding { Source = _textBox, Path = "Text" });

            // The following line will also do the trick, but you won't be able to unbind.
            //_textBlock[!TextBlock.TextProperty] = _textBox[!TextBox.TextProperty];
        }
    }

    private void UnbindButton_Click(object? sender, RoutedEventArgs e)
    {
        _bindingSubscription?.Dispose();
        _bindingSubscription = null;
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}

绑定是通过调用TextBlock上的Bind方法来实现的:

_bindingSubscription =
  _textBlock.Bind(TextBlock.TextProperty, new Binding { Source = _textBox, Path = "Text" });

它返回存储在_bindingSubscription字段中的一次性对象。

为了破坏绑定——这个对象必须被处理掉:_bindingSubscription.Dispose()

令人惊讶的是(至少对于真正的你来说),以下C#代码也将建立相同的绑定:

_textBlock[!TextBlock.TextProperty] = _textBox[!TextBox.TextProperty];  

只有这样的绑定是不可破坏的(或者至少不像Bind(...)方法返回的那样容易破坏)。

经过一番研究,我明白了这是如何工作的:bang (!)运算符将AvaloniaProperty对象转换为类型IndexerDescriptor的对象。可以将此对象传递给AvaloniaObject's运算符[]以返回类型为IBinding的对象。然后对另一个AvaloniaObject上的IndexerDescriptor单元格进行赋值将调用Bind(...)方法并创建绑定。

绑定到非可视类的属性

之前,我们展示了在视觉对象上绑定两个(源和目标)属性的不同方法。然而,绑定源不必在可视对象上定义。事实上,正如我们之前在非常重要和流行的MVVM模式下提到的,复杂的视觉对象正在被用来模仿简单的非视觉对象的行为——所谓的ViewModel

在本小节中,我们将展示如何在非可视类中创建可绑定属性并将我们的视觉对象绑定到它们。

该项目位于NP.Demos.BindingToNonVisualSample.sln。这是您在运行它时看到的内容:

中间有一个名字列表。姓名的数量显示在左下方,删除姓氏的按钮位于右下方。

单击该按钮可删除列表中的最后一项。您会看到列表和项目数量将得到更新。当您从列表中删除所有项目时,项目数将变为0,并且按钮将被禁用:

此示例的自定义代码位于三个文件中:ViewModel.csMainWindow.axamlMainWindow.axaml.csViewModel是一个非常简单的纯非视觉类。这是它的代码:

public class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged(string propName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }

    // collection of names
    public ObservableCollection<string> Names { get; } = new ObservableCollection<string>();

    // number of names
    public int NamesCount => Names.Count;

    // true if there are some names in the collection,
    // false otherwise
    public bool HasItems => NamesCount > 0;

    public ViewModel()
    {
        Names.CollectionChanged += Names_CollectionChanged;
    }

    // fire then notifications every time Names collection changes.
    private void Names_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
    {
        // Change Notification for Avalonia for properties
        // NamesCount and HasItems
        OnPropertyChanged(nameof(NamesCount));
        OnPropertyChanged(nameof(HasItems));
    }
}  

请注意,该集合Names的类型为ObservableCollection<string>。这确保了绑定到该Names集合的可视集合能够在从非可视Names集合中添加或删除项目时自行更新。

另请注意,每次Names集合更改时,我们都会触发传递给它们的nameof(NamesCount)nameof(HasItems)作为参数的PropertyChanged事件。这将通知绑定到那些他们必须更新其目标的属性。

现在看看MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:NP.Demos.BindingToNonVisualSample"
        ...>
  <!-- Define the DataContext of the Window-->
  <Window.DataContext>
    <local:ViewModel>
      <local:ViewModel.Names>
        <x:String>Tom</x:String>
        <x:String>Jack</x:String>
        <x:String>Harry</x:String>
      </local:ViewModel.Names>
    </local:ViewModel>
  </Window.DataContext>
  <Grid ...>

    <!-- Binding the Items of ItemsControl to the Names collection -->
    <ItemsControl Items="{Binding Path=Names}"
                  .../>

    <Grid Grid.Row="1">

      <!-- Binding Text to NamesCount -->
      <TextBlock Text="{Binding Path=NamesCount, StringFormat='Number of Items: {0}'}"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Center"/>

      <!-- Binding Button.IsEnabled to HasItems -->
      <Button x:Name="RemoveLastItemButton"
              Content="Remove Last Item"
              IsEnabled="{Binding Path=HasItems}"
              .../>
    </Grid>
  </Grid>
</Window>

Window DataContext被直接设置为包含一个ViewModel类型的对象,其Names集合填充为TopJackHarry。由于DataContext沿Visual Tree传播,MainWindow.asaml文件中的其余元素将具有相同的DataContext

ItemControl's Items属性绑定到ViewModel对象的Names集合:<ItemsControl Items="{Binding Path=Names}"。请注意,在WPF中,ItemsControl将改为使用ItemsSource属性。

TextBlock's Text属性绑定到ViewModel<TextBlock Text="{Binding Path=NamesCount, StringFormat='Number of Items: {0}'}"NamesCount属性。注意在绑定中StringFormat的使用——它允许在绑定值周围添加一些string

最后,将Button's IsEnabled属性绑定到ViewModel上的HasItems属性,使项目数变为'0',按钮变为禁用状态。

最后,MainWindow.xaml.cs文件仅包含设置事件处理程序以在每次单击按钮时从Names集合中删除最后一项:

public MainWindow()
{
    InitializeComponent();

    ...

    Button removeLastItemButton =
        this.FindControl<Button>("RemoveLastItemButton");

    removeLastItemButton.Click += RemoveLastItemButton_Click;
}

private void RemoveLastItemButton_Click(object? sender, RoutedEventArgs e)
{
    ViewModel viewModel = (ViewModel)this.DataContext!;

    viewModel.Names.RemoveAt(viewModel.Names.Count - 1);
}  

结论

本文致力于最重要的Avalonia概念,其中许多概念来自WPF,但在Avalonia中得到了扩展并变得更好、更强大。

那些想要正确理解和使用Avalonia的人应该阅读、通读并理解这些概念。

我计划写另一篇文章,或者其中几篇解释更高级的Avalonia概念,特别是:

  1. 路由事件
  2. 命令
  3. 控制模板(基础的)
  4. MVVM模式、DataTemplates、ItemsPresenter和ContentPresenter
  5. 从XAML调用C#方法
  6. XAML——通过标记扩展重用Avalonia XAML
  7. 样式、转换、动画

https://www.codeproject.com/Articles/5311995/Multiplatform-Avalonia-NET-Framework-Programming-B

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Avalonia是一个**跨平台的UI框架,用于创建桌面应用程序**。 以下是一些关于Avalonia的基本信息和学习资源: 1. **什么是Avalonia?**:Avalonia是一个基于WPF XAML的开源UI框架,它允许开发者使用.NET构建跨平台的桌面应用程序。Avalonia支持多种操作系统,包括Windows、Linux和macOS。 2. **准备工作**:在开始使用Avalonia之前,你需要安装相应的开发环境,并配置项目。这通常包括安装.NET SDK和Avalonia工具包。 3. **创建第一个Avalonia应用程序**:你可以通过官方文档或相关教程来创建你的第一个Avalonia应用,这将帮助你理解基本的应用程序结构和开发流程。 4. **XAML基础**:XAML是一种用于定义用户界面的语言,你可以学习如何使用XAML来创建界面布局和实现数据绑定。 5. **控件和样式**:Avalonia提供了丰富的控件库,你可以学习如何使用这些控件以及如何通过样式和模板来自定义它们的外观。 6. **MVVM模式**:MVVM(Model-View-ViewModel)是一种设计模式,用于分离应用程序的业务逻辑和界面表示。学习MVVM将有助于你构建可维护和可测试的应用程序。 7. **导航和多窗口**:了解如何在Avalonia进行窗口导航和管理多个窗口,这对于构建复杂的桌面应用程序非常重要。 8. **打包和发布应用程序**:最后,你需要学习如何将你的Avalonia应用程序打包和发布,以便用户可以在他们的计算机上安装和使用你的应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值