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

2023-11-11

目录

介绍

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 我可以禁用特定控件的键盘输入吗?

    是否可以禁用控件的键盘输入 例如一个ListView 我怎么做 我尝试过覆盖KeyUp KeyDown事件 但显然不是这样的 IsEnabled是一个很好的解决方案 但是我只想禁用键盘交互并保持鼠标交互不变 处理KeyDown事件来得太晚了
  • 如何在 RichTextBox 中以编程方式移动插入符位置?

    我有一个 RichTextBox 其中的特殊文本位具有自定义格式 但是 存在一个错误 即插入字符后 插入符号会放置在新插入的字符之前而不是之后 这是因为对于每次编辑 代码都会重新计算内容以应用自定义格式 然后像这样设置 CaretPosit
  • 列表视图上的 TextBlock:如何忽略 TextBlock 中的点击并让列表视图处理它们

    我有一个显示大量信息的列表视图 但是当它为空时 我想在其上覆盖一个文本块 上面写着 没有要显示的信息 或 bla bla bla 添加信息 列表视图设置为响应鼠标单击 但现在如果我单击文本块 这些事件将路由到文本块 我怎样才能让这些事件转到
  • 在 xaml 中编写嵌套类型时出现设计时错误

    我创建了一个用户控件 它接受枚举类型并将该枚举的值分配给该用户控件中的 ComboBox 控件 很简单 我在数据模板中使用此用户控件 当出现嵌套类型时 问题就来了 我使用这个符号来指定 EnumType x Type myNamespace
  • WPF 数据绑定到复合类模式?

    我是第一次尝试 WPF 并且正在努力解决如何将控件绑定到使用其他对象的组合构建的类 例如 如果我有一个由两个单独的类组成的类 Comp 为了清楚起见 请注意省略的各种元素 class One int first int second cla
  • 如何在C#背后的代码中动态创建数据模板并绑定TreeView分层数据

    我有一个场景 其中树视图动态更改其数据模板和数据绑定定义 我在 XAML 中创建了一个树视图 如下所示
  • Winforms 中的 WPF ElementHost 最大化时崩溃 (Windows)

    我正在尝试将新的 WPF 控件集成到现有的 WinForms 应用程序中 并使用 ElementHost Dock Fill 来托管以下 XAML UserControl NET 4 当我将 WinForm 设置为最大化时 我的整个操作系统
  • WPF:Prism 对于小型应用程序来说是不是太过分了?

    如果我不将我的应用程序分成不同的模块 否则我会认为 Prism 确实是可行的方法 我应该使用 Prism 吗 我知道 Prism 提供了一个方便的实现ICommand 我可以自己在一页代码中完成 并为我们提供IEventAggregator
  • WPF - 如何从 DataGridRow 获取单元格?

    我有一个具有交替行背景颜色的数据绑定 DataGrid 我想根据单元格包含的数据对单元格进行不同的着色 我已经尝试过该线程建议的解决方案 http wpf codeplex com Thread View aspx ThreadId 511
  • 如何在wpf中翻转图像

    我最近学习了如何使用 TransformedBitmap 和 RotateTransformed 类旋转 BitmapImage 现在我可以对图像进行顺时针旋转 但如何翻转图像呢 我找不到执行 BitmapImage 水平和垂直翻转的类 请
  • 如何枚举控件的所有依赖属性?

    我有一些 WPF 控件 例如 文本框 如何枚举该控件的所有依赖属性 如 XAML 编辑器所做的那样 不需要使用反射 恕我直言 这是一个坏主意 因为框架已经为此提供了实用程序类 但它们并不明显找到 以下是基于这篇文章的答案 枚举绑定 http
  • 如何以编程方式调用应用程序菜单?

    我有自定义样式的非矩形透明窗口
  • WPF 创建同级窗口并关闭当前窗口

    我需要的是我的窗口类中的这样一个事件处理程序 void someEventHandler object sender RoutedEventArgs e MyNewWindow mnw new MyNewWindow mnw Owner W
  • 解释 System.Diagnostics.CodeAnalysis.SuppressMessage

    我在某些应用程序中有这种代码 来自微软 assembly System Diagnostics CodeAnalysis SuppressMessage Microsoft Naming CA1702 CompoundWordsShould
  • 创建带有部分的选项卡式侧边栏 WPF

    我正在尝试创建一个带有部分的选项卡式侧边栏 如 WPF 中的以下内容 我考虑过几种方法 但是有没有更简单 更优雅的方法呢 方法一 列表框 Using a ListBox并将 SelectedItem 绑定到右侧内容控件所绑定的值 为了区分标
  • 如何部署“SQL Server Express + EF”应用程序

    这是我第一次部署使用 SQL Server Express 数据库的应用程序 我首先使用实体 框架模型来联系数据库 我使用 Install Shield 创建了一个安装向导来安装应用程序 这些是我在目标计算机中安装应用程序所执行的步骤 安装
  • 用于打印的真实尺寸 WPF 控件

    我目前正在开发一个应用程序 用户可以在画布上动态创建 移动 TextBlock 一旦他们将文本块放置在他们想要的位置 他们就可以按下打印按钮 这将导致 ZPL 打印机打印当前显示在屏幕上的内容 ZPL 命令是通过从每个 TextBlock
  • Azure 可以运行 WPF 吗?

    我想编写一个在 Windows Azure 上运行的 ASP Net MVC 应用程序 该应用程序将使用 WPF 创建图像 在我开始写之前 这会起作用吗 Azure 是否具有渲染 WPF 所需的 DLL 包括 DirectX 和图形功能 我
  • 使用 WPF 和数据绑定将文件拖放到应用程序窗口中

    我希望能够将文件 例如从桌面或资源管理器 拖放到 WPF 应用程序的主窗口中 我也不希望有任何代码隐藏 即我想使用数据绑定 到目前为止 我测试了 gong wpf dragdrop 它似乎不支持应用程序外部的拖动目标 我可以将文件拖放到主窗
  • 将两个垂直滚动条相互绑定

    我在控件中有两个 TextBox 并且它们都有两个 VerticalScrollBar 我想在它们之间绑定 VerticalScrollBars 如果一个向上 第二个也会向上等等 如果可以的话我该怎么做 Thanks 不是真正的绑定 但它有

随机推荐

  • register的用法c语言,C语言中auto,static,register,extern存储类型的用法

    在C语言中提供的存储类型说明符有auto extern static register 说明的四种存储类别 四种存储类别说明符有两种存储期 自动存储期和静态存储期 其中auto和register对应自动存储期 具有自动存储期的变量在进入声明
  • Unity脚本API—Transform 变换

    场景中的每一个对象都有一个Transform 用于储存并操控物体的位置 旋转和缩放 每一个Transform可以有一个父级 允许你分层次应用位置 旋转和缩放 可以在Hierarchy面板查看层次关系 他们也支持计数器 enumerator
  • AGV调度系统/两阶段算法模拟源代码 地图建模

    多 AGV调度系统 两阶段算法模拟源代码 地图建模c openTCS1 AGV调度系统源码 OpenTCS OpenTCS是一个开源的AGV调度系统程序 能给初入AGV行业的人士一些帮助 该实例是包含源码的程序 可成功运行 2 openTC
  • UE4 蓝图数组反转节点ReverseforEachLoop的坑

    由于蓝图没有直接反转数组的节点类似于C 的reverse 所以只好用ReverseforEachLoop这个节点 但是这个节点有个坑 就是ArrayIndex是从数组末尾元素下标开始输出 并非是从0开始 因此如果要用到ArrayIndex得
  • 记录一次JVM发生OOM问题的解决,包括jvisualvm工具的使用,以及对GC的理解等

    目录 OOM原因分析 解决 利用hprof文件分析的步骤 结合jvisualvm工具 jvisualvm实时监控本地jvm jvisualvm实时监控远程jvm 对于JVM的GC理解 Minor GC Full GC 常用JVM参数配置 查
  • WIN32 资源

    首先解释一下句柄 win32中的句柄在数值上表示一个32位的数 用来标识应用程序 进程中不同对象以及同类对象中的不同实例 而所谓实例就是指被实例化的对象 实例化的过程就是通过类创建对象的过程 实例化对象的目地是为对象开辟内存空间 所以句柄是
  • Java - 常用类 - BigInteger 和 BigDecimal

    文章目录 BigInteger 和 BigDecimal 介绍 应用场景 BigInteger 和 BigDecimal 介绍 应用场景 BigInteger适合保存比较大的整型 BigDecimal适合保存精度更高的浮点型 小数 pack
  • 智能人机交互

    前言 随着移动机器人越来越多地走向实 际应用 需要提高机器人与人类之 间的协同水平 实现机器人与人类的共融 一 人机交互的三个级别 二 火星车的遥操作控制 火星车的遥操作控制 超大时延 地面团队将命令序列发至火 星车 如要求火星车往前行驶1
  • C# 进度条使用

    前言 介绍C 自带的Progressbar控件的调用方法 对程序运行的进度进行提示 内容 接下来 介绍进度条在Winform界面中具体的应用和步骤 在项目中新建一个Winform界面 上面放一个ProgressBar和label控件 默认命
  • SourceInsight4.0.0124中文版-黑色背景主题

    此背景目前只在SI 4 0 0124中文版试过 此黑色主题是自己改的 亲测可用 只适用于4 0 0124中文版 4 0 0124中文版 4 0 0124中文版 其他中文版的没试过 此主题不适用英文版 也没必要用在英文版 因为英文版本身已自带
  • 基于TensorFlow Lite实现的Android花卉识别应用

    介绍 本教程将在Android设备上使用TensorFlow Lite运行图像识别模型 具体包括 使用TensorFlow Lite Model Maker训练自定义的图像分类器 利用Android Studio导入训练后的模型 并结合Ca
  • QT怎么实现HTTP同步

    Qt 提供了 QNetworkAccessManager 类可以用于实现 HTTP 同步请求 使用该类可以很方便地实现同步的 HTTP 请求 并可以直接获取响应的内容 下面是一个示例代码 include
  • myeclipse中设置代码注释模板

    1 设置模板 文件 Files 注释标签 Title file name Package package name Description todo author user date date 类型 Types 注释标签 类的注释 Clas
  • VMware提示此主机支持Intel VT-x,但Intel VT-x处于禁用状态——解决方法

    虚拟机VMware提示此主机支持Intel VT x 但Intel VT x处于禁用状态 也就是需要开启Intel Virtualization Technology虚拟化技术 Intel VT x完整名称是Intel Virtualiza
  • python安装opencv出现如下错误:Could not find a version that satisfies the requirement cv2 (from versions: )

    如题所示在python中安装cv2库是提示不能找到满足需要的版本 我的环境配置是 pycharm anaconda3 对应的python版本是python3 6 之前想着在pycharm中直接安装的 即打开项目对应的解释器设置模块 然后安装
  • c++ max() 报错 error: no matching function for call to ‘max’

    先举个小例子哈 我要统计字符串数组中最长字符串的长度 include
  • js检测字段中首个字符是否为字母

    var sSrc w33333 var sASC sSrc charCodeAt 0 if sASC gt 65 sASC lt 90 sASC gt 97 sASC lt 122 代码 A Z的ascii码 bai65 90 a z的as
  • Mybatis框架的基本知识梳理

    Mybatis框架的基本知识梳理 一 原始JDBC开发存在的问题 import org junit Test import java math BigDecimal import java sql public class JdbcTest
  • JavaScript详解DOM和BOM(持续更新)

    目录 1 DOM简介 1 1什么是DOM 1 2DOM树 2 如何获取页面元素 2 1根据id获取 2 2根据标签名获取 2 3通过HTML5新增方法获取 ie9以上支持 2 3 1根据类名获取元素的集合 2 3 2querySelecto
  • 简单示例中的多平台Avalonia .NET Framework编程基本概念

    目录 介绍 关于Avalonia 本文的目的 本文的组织 示例代码 解释概念 视觉树 Avalonia 工具 逻辑树 附加属性 样式属性 直接属性 有关附加 样式和直接属性的更多信息 绑定 什么是Avalonia UI和WPF中的绑定以及为