目录
介绍
关于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是一个新的开源包,它与WPF非常相似,但与WPF或UWP不同,它适用于大多数平台——Windows、MacOS 和各种Linux版本,并且在许多方面都比WPF更强大。
avalonia的源代码可在GitHub上的Avalonia源代码上找到。
有一些可用的Avalonia文档虽然并不广泛,但应该会快速改进。
Avalonia在Gitter上有一个不错的免费公共支持:Gitter 上的Avalonia以及在Avalonia Support 购买商业支持的一些选项 。你也可以在Avalonia Github Discussions中提问。
Avalonia是比Web编程框架或Xamarin更好的框架的原因在上一篇文章中有详细描述:在Easy Samples中使用AvaloniaUI进行多平台UI编码——第1部分——AvaloniaUI构建块。在这里,我只总结两个主要原因:
本文的主要目的是为那些不一定了解WPF的人解释最重要的Avalonia/WPF概念。对于WPF专家,本文将作为通往Avalonia的门户。
我试图通过提供解释、详细图片和简单的Avalonia示例来阐明这些概念,尽可能突出这些概念。
本文将涵盖以下主题:
以下主题将留给以后的文章:
示例代码位于Avalonia概念文章的演示代码下。这里的所有示例都在Windows 10、MacOS Catalina和Ubuntu 20.4上进行了测试
所有代码都应该在Visual Studio 2019下编译和运行——这就是我一直在使用的。此外,请确保在第一次编译示例时您的Internet连接已打开,因为必须下载一些nuget包。
Avalonia(和WPF)基本构建块(基元)包括:
其余控件(更复杂的控件,包括诸如Button、ComboBox、Menu等的基本控件)和复杂视图是通过将各种基元放在一起并将它们放置在其他基元或面板中来构建的。在Avalonia中,基元通常从Control类继承,而更复杂的控件从TemplatedControl类继承,而在WPF中,基元继承自Visual,更复杂的控件继承自Control(在WPF中,Control有Template属性和相关的基础设施,而在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的内容:
还请看一下App.axaml文件。您将看到对以下FluentTheme内容的引用:
主题定义了所有主要控件的外观和行为,当然包括按钮。他们通过使用样式和模板来做到这一点,具体如何——稍后将在这些系列文章中进行解释。重要的是要理解,我们的Button视觉树是由Button的ControlTemplate定义的,它位于Button样式中,而按钮的样式又位于FluentTheme中。
这是您在运行项目时看到的内容:
单击窗口以获得鼠标焦点,然后按F12键。Avalonia工具窗口将打开:
工具窗口类似于WPF snoop(尽管它在某些方面仍然不如WPF snoop强大)。它使您能够调查视觉树或逻辑树中任何元素的任何属性或事件。
在Avalonia中,逻辑树(稍后将提供它的解释)比WPF中发挥更大的作用,因此默认情况下,该工具显示逻辑树,为了切换到可视树,您需要单击“视觉树”选项卡(在上图中由读取的椭圆突出显示)。
一旦我们切换工具以显示可视树,同时按下Control和Shift键并将鼠标放在按钮的文本上。工具左侧的可视化树将扩展为包含Button的文本的元素,工具中间的属性窗格将显示可视化树的当前选定元素的属性(在我们的示例中为Button的TextBlock元素):
可视化树实际上是针对整个窗口显示的(其中与当前选定元素对应的部分被展开)。
您可以看到来自FluentTheme的Button的Visual 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
请注意,为了使Visual Tree扩展方法可用,我们必须在MainWindow.axaml.cs文件的顶部添加using Avalonia.VisualTree;命名空间引用。
在方法的最后放置一个断点,然后单击按钮。您可以在Watch窗口中调查OnButtonClick()方法内变量的内容:
可以看到结果与Tool中观察到的Visual Tree一致:
的确,
一旦我们在上一节中提到了Avalonia工具,让我们在这里提供更多关于它的信息。
该工具的美妙之处在于它也是用Avalonia编写的,因此它是多平台的。如果您想检查这些平台上的树和属性,它也会在MacOS和Linux上显示——您需要做的就是单击您希望该工具工作的窗口并按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代码:
我们看到窗口的内容由两行的Grid面板表示。顶行包含Button “Click Me”,底行包含ItemsControl两个按钮:“Item1 Button”和“Item2 Button”。按钮的XAML名称与其中写入的名称相同,只是没有空格:“ClickMeButton”、“Item1Button”和“Item2Button”。
单击示例窗口,然后按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("TheItemsControl");
var logicalParent = itemsControl.GetLogicalParent();
var logicalAncestors = itemsControl.GetLogicalAncestors().ToList();
var logicalChildren = itemsControl.GetLogicalChildren().ToList();
var logicalDescendants = itemsControl.GetLogicalDescendants().ToList();
}
我们正在获取ItemsControl元素的逻辑父、祖先、子孙和后代。此方法设置为 “ClickMeButton”的Click事件处理程序:
Button clickMeButton = this.FindControl
请注意,为了获得这些扩展方法,我们必须在MainWindow.xaml.cs文件的顶部添加using Avalonia.LogicalTree命名空间。
在方法的最后放置一个断点,运行应用程序并按下“ClickMeButton”。检查Watch窗口中的变量:
这与我们在工具中看到的完全对应。
附加属性是一个非常重要且有用的概念,需要理解。它最初是由WPF引入的,并从那里直接进入了Avalonia,尽管它是一个更好和扩展的版本。
为了解释附加属性是什么,让我们首先记住什么是C#中的简单读/写属性。本质上,MyClass类中定义的类型T属性可以由两种方法表示——getter和setter方法:
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 hashtable或Dictionary(或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 MyAttachedPropertyDictionary =
new Dictionary();
// 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中的实体。
这种方法节省了大量内存,但代价是属性的getter和setter稍慢。
一旦尝试了附加属性,就会发现除了节省内存之外,它们还提供了许多其他好处——例如:
当然,上面显示的非常简单的实现,并没有考虑很多其他问题,如线程、回调、注册(以便了解我们类MyClass上允许的所有附加属性)等等。此外,像我们上面所做的那样,将默认值本身定义为属性之外的static变量是很难看的。由于这些考虑,创建一个特殊类型AttachedProperty<...>(可能带有一些通用参数)是有意义的,它将包含Dictionary、默认值和属性运行所需的许多其他功能。这就是WPF和Avalonia所做的。
在我们继续Attached Property示例之前,最好下载我在Avalonia Snippets上提供的Avalonia片段并安装它们。可以在相同的URL中找到安装说明。
附加属性示例位于NP.Demos.AttachedPropertySample.sln解决方案下。尝试运行它。这是您将看到的内容:
滑块的变化可以在值0和10之间变化,并且当您更改滑块位置时,矩形的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 type. This field contains the
// Attached Properties' Dictionary, the default value and the rest of the required
// functionality
public static readonly AttachedProperty RectangleStrokeThicknessProperty =
AvaloniaProperty.RegisterAttached
我们可以看到:
定义附加属性所需的代码量看起来很庞大,但它们都是在avap代码段的帮助下在几秒钟内创建的。因此,如果您计划使用Avalonia——片段是必须的(与WPF中相同)。您还可以看到我的代码片段将每个附加属性放在自己的区域中,以便可以折叠它并使代码更具可读性。
现在,看看MainWindow.cs文件中的XAML代码:
请注意,在XAML中,我使用的是绑定——一个非常重要的概念,稍后将更详细地解释。
我们有一个Grid有两行的面板——顶行有一个Rectangle,底部有一个Slider控件。Slider可以在0和10之间更改其值。
几乎在最顶端——Window XAML标记中有以下行:
xmlns:local="clr-namespace:NP.Demos.AttachedPropertySample"
这一行定义了本地XAML命名空间,以便通过该命名空间,我们可以引用我们的RectangleStrokeThickness附加属性。
下一个有趣的行是:
local:AttachedProperties.RectangleStrokeThickness="7"
在这里,我们将窗口对象上的RectangleStrokeThickness附加属性的初始值设置为数字7。请注意我们如何指定附加属性:
代码行...
StrokeThickness="{Binding Path=(local:AttachedProperties.RectangleStrokeThickness),
RelativeSource={RelativeSource AncestorType=Window}}"
... 在Rectangle标签下,将矩形的属性StrokeThickness绑定到矩形窗口祖先上的附加属性RectangleStrokeThickness。请注意在Binding中的Attached Property格式——Attached Property全名在 paratheses内——这是Avalonia和WPF中的要求——如果没有paratheses,绑定将不起作用,人们可能会花费数小时试图找出问题所在。
Slider的代码行:
Value="{Binding Path=(local:AttachedProperties.RectangleStrokeThickness),
Mode=TwoWay,
RelativeSource={RelativeSource AncestorType=Window}}"
将Slider的Value属性绑定到滑块的窗口祖先的RectangleStrokeThickness附加属性(当然,这是与Rectangle的窗口祖先相同的Window对象)。这个绑定是一个TwoWay绑定——意味着对Slider的Value属性的更改也会更改Window上的RectangleStrokeThickness附加属性值。
这个视图的操作原理很简单——通过移动Slider的所谓的thumb来改变Slider的值——将触发Window上的RectangleStrokeThickness附加属性的变化(通过Slider的绑定),这反过来又会触发更改Rectangle上的StrokeThickness属性(通过其绑定)。
当然,在这个简单的例子中,我们可以直接将 Slider的Value连接到Rectangle的StrokeThickness属性而不涉及Window上的Attached Property,但是该示例不会演示Attached Properties是如何工作的(在许多情况下,例如,当控件上不存在所需的属性附加属性是必须的)。
现在尝试删除顶部将初始值设置为7的行:
local:AttachedProperties.RectangleStrokeThickness="7"
并重新启动应用程序。你会看到Rectangle的StrokeThickness和Slider的Value初始值变成了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 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
您可以在方法的末尾放置一个调试断点,在调试器上启动应用程序并尝试移动滑块——您将在断点处停止并能够调查当前值。
另一种更简单的方法(不能终止订阅)是在MainWindow.axaml.cs文件中创建一个static构造函数并使用AddClassHandler扩展方法:
static MainWindow()
{
AttachedProperties
.RectangleStrokeThicknessProperty
.Changed
.AddClassHandler((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有一个依赖属性的概念,它与附加属性基本相同,只是它们定义在使用它们的同一个类中,相应地,它们的getter和setter放在同名的class属性中。请注意,使用依赖属性,我们仍然具有不会在默认值上浪费内存和轻松添加回调的优势,但是我们失去了在不修改类的情况下添加属性的优势。
我尝试在Avalonia中使用本地定义的附加属性,但我没有发现它们有任何问题,但根据Avalonia文档,最好使用所谓的样式属性(为什么——我现在不确定)。
我们将按照文档并运行一个示例来展示如何使用所谓的样式属性。
对于示例,打开NP.Demos.StylePropertySample.sln解决方案。
该示例将以与前一个完全相同的方式运行,并且代码非常相似,只是我们不使用在AttachedProperties.cs文件中定义的RectangleStrokeThickness附加属性,而是使用在MainWindow.axaml.cs文件中定义的同名的Style属性。您可以看到Style Property的getter和setter是非静态的,并且相当简单:
#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 RectangleStrokeThicknessProperty =
AvaloniaProperty.Register
(
nameof(RectangleStrokeThickness)
);
#endregion RectangleStrokeThickness Styled Avalonia Property
这个style属性也是使用我的其他代码片段在几秒钟内创建的——avsp代表Avalonia样式属性。
有时,想要使用一个由字段支持的简单C#属性,但又能够订阅其更改并将该属性用作某些绑定的目标——是的,只有Attached、Style和Direct属性可以用作目标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 RectangleStrokeThicknessProperty =
AvaloniaProperty.RegisterDirect
(
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类。
如上所述,只有Attached、Style 和Direct属性可以作为Avalonia UI绑定的目标。
Attached、Style和Direct属性只能在AvaloniaObject实现的类上设置——这是所有Avalonia视觉效果都实现的非常基本的类。
如果您不需要更改变量的先前值(在我们上面的OldValue示例中),订阅Attached、Style和Direct属性更改的最佳方法是使用该AvaloniaObject.GetObservable(AvaloniaProperty property)方法。
为了演示使用GetObservable(...)方法,我们可以修改我们的附加属性示例,如下所示:
public MainWindow()
{
...
_changeNotificationSubscriptionToken =
this.GetObservable(AttachedProperties.RectangleStrokeThicknessProperty)
.Subscribe(OnStrokeThicknessChanged);
}
private void OnStrokeThicknessChanged(double newValue)
{
...
}
您可以看到OldValue在回调中不再可用。
绑定是一个非常强大的概念,它允许绑定两个属性,这样当其中一个属性发生变化时,另一个也会发生变化。通常,绑定从source属性到target属性——正常OneWay绑定,但也有一个TwoWay绑定可以确保两个属性同步,无论哪个发生变化。还有另外两种绑定模式:OneWayToSource和OneTime使用频率较低的绑定。
也很少讨论,但同样重要的集合绑定,其中一个集合模仿另一个集合,或者两个集合互相模仿。
请注意,绑定的目标不必与绑定的源完全相同,可以在源和目标之间进行转换,反之亦然,如下所示。
绑定是所谓的MVVM模式背后的主要概念(将在以后的一篇文章中详细讨论)。MVVM模式的核心思想是复杂的视觉对象模仿非常简单的非视觉对象的属性和行为——即所谓的视图模型(VM):
正因为如此,大部分业务逻辑可以在简单的非可视对象上开发和测试,然后通过绑定传输到非常复杂的可视对象,该对象将自动以类似的方式运行。
与WPF绑定相比,Avalonia绑定更强大、更少错误和古怪且更易于使用——原因是它们是由非常聪明的人(或人)最近构建的,显然喜欢WPF绑定的Steven Kirk知道他们的怪癖和限制,此外还了解软件开发理论和实践的最新进展——反应式扩展。
关于Avalonia绑定的另一个好处是,与许多其他Avalonia功能不同,它们有很好的文档记录:在Avalonia Data Bindings Documentation。
综上所述,我认为展示如何在真实的C#/XAML示例中创建各种绑定将很有用,特别是对于那些没有WPF经验的人。
Avalonia Binding是一个复杂的对象,具有许多功能,其中一些最重要的功能,我将在本小节中讨论。
下图最好地解释了Avalonia(和WPF)绑定:
以下绑定部分很重要:
在Avalonia和WPF中还有一个所谓的MultiBinding。MultiBinding假设有多个绑定源,但仍然是同一个绑定目标。多个源由一个特殊的转换器组合成一个目标,该转换器在多重绑定的情况下实现IMultiValueConverter。
绑定的复杂部分之一是在Avalonia和WPF中都有多种方法可以指定源对象,但Avalonia有更多方法可以做到这一点。以下是指定源对象的各种方法的说明:
现在理论已经足够了,让我们做一些实际的例子。
此示例位于NP.Demos.BindingSourcesSample.sln解决方案中。此示例显示了在XAML中设置绑定源的各种可能方法。
这是您在运行示例后看到的内容:
现在让我们一一浏览各种示例(所有示例都位于MainWindow.axaml文件中)并解释生成它的XAML代码。
...
...
当绑定中没有指定源时,Binding的源恢复为元素的DataContext属性。在我们的示例中,在Window上设置了DataContext,但由于它沿可视树传播(除非明确更改)——我们的TextBlock有相同的DataContext——这只是由我们的TextBlock显示的一个简单string。
在我们的第二个示例中,我们使用StaticResource标记扩展将绑定的源设置为字符串“This is the Window's resource”,该字符串定义为Window的资源。
This is the Window's resource
...
...
我们的窗口有XAML名称——“TheWindow”,我们使用它来绑定到它的Tag: (Tag是在每个Avalonia Control上定义的属性,它可以包含任何对象。)
...
...
以上Text={Binding Path=Tag, ElementName=TheWindow}是Avalonia的简写。
这个示例展示了元素如何在Self模式下使用RelativeSource将自己作为Binding的源对象。
使用TemplatedParent模式的RelativeSource只能在ControlTemplate内部使用,并且使用它意味着绑定引用在当前模板实现的控件上定义的属性(或路径):
上面的代码意味着我们绑定到由ControlTemplate实现的TemplatedControl上的Tag属性。
指定AncestorType将向Binding表示RelativeSource处于FindAncestor模式。
使用AncestorLevel,您可以指定不需要所需类型的第一个祖先,而是第N个——其中N可以是任何正整数。
在下面的代码中,我们在元素的祖先中搜索第二个Grid:
请注意,$parent.Tag意味着找到元素的父级(第一个祖先)并从中获取Tag属性。此绑定应等效于更长的版本:
$parent[Grid].Tag成功了。
$parent[Grid;1]引用类型Grid的第二个祖先。这里有一个不一致的地方——祖先的编号在视觉树中从1开始,但在逻辑树中从0开始。
此示例位于NP.Demos.BindingModesSample.sln解决方案下。此示例的所有代码都位于MainWindow.asaml文件中。
运行示例,您将看到以下内容:
前三个TextBoxes绑定到相同的Window的Tag属性——第一个使用TwoWay模式,第二个——OneWay和第三个—— OneTime。尝试在顶部TextBox输入。然后,顶部的第二个TextBox将得到更新,但不是第三个:
这是可以理解的,因为顶部TextBox有一个使用Window's标签的TwoWay绑定——当你修改它的文本时,Window的标签也会被更新,并且绑定到同一个标签的一种方式将更新第二个TextBox。
如果您尝试在第二个TextBox中修改文本,则不会发生任何事情,因为它具有OneWay——从Window的Tag到TextBox.Text的绑定。当然,当有人修改第三个TextBox中的文本时,什么都不会发生。
这是前三个文本框的相关代码(第第四个是特殊的,我会解释——为什么——稍后)。
...
...
第四是TextBox示范OneWayToSource模式。请注意,最初,它不显示任何内容。如果你开始输入它,你会看到下面出现了相同的文本:
这是第四个TextBox的相关代码:
...
TextBox和TextBlock都绑定到Grid panel上的Tag。
请注意,Tag最初有一些文本:“这是一个OneWayToSource网格标签”。然而,TextBox和TextBlock一开始都是空的。这是因为OneWayToSource绑定删除了标签的初始值(TextBox最初没有任何文本在其中,因此它覆盖了绑定的Tag初始值)。
这就是我没有在第四个TextBox中使用Window 的Tag的原因——它会破坏其他三个TextBoxes的初始值。
这也是我很少使用OneWayToSource绑定的原因——如果它从Source分配初始值给Target,并且只有这样才会从Target工作到Source,那么它会有用得多。
打开NP.Demos.BindingConvertersSample.sln解决方案。这是您运行后将看到的内容:
尝试从顶部TextBox删除文本。绿色文本将消失,而将出现红色文本:
此外,无论您在顶部或底部TextBox键入什么,相同的字符但从右到左倒置将出现在另一个TextBox中。
以下是相关代码:
...
对于这两个TextBlocks,我使用的是Avalonia内置转换器——IsNullOrEmpty和IsNotNullOrEmpty。它们被定义为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。它们的串联将继续显示在底部。
这是执行此操作的相关代码:
MultiBinding包含两个到单个文本框的单值绑定:
它们的值由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
该类实现了IMultiValueConverter接口(不是IValueConverter用于单值绑定转换)。
IMultiValueConverter只有一个方法——Convert(...)用于前向转换,它的第一个参数是IList,其每个源值都有一个条目。
为了避免通过创建XAML资源来污染XAML代码,我创建了一个名Instance为的static属性,该属性引用同一类的全局实例,并且可以通过x:Static标记扩展从XAML轻松访问:Converter="{x:Static local:ConcatenationConverter.Instance}"。
下一个示例位于NP.Demos.BindingInCode.sln解决方案下。这是您运行后将看到的内容:
尝试更改中的文本——在您按下按钮“绑定TextBox”之前不会发生其他任何事情。按下它后,文本将出现在模仿其中的文本TextBox下方:
当您按下按钮“取消绑定”时,下面的文本将再次停止对修改做出反应。
此功能主要由MainWindow.asaml.cs中的代码实现。XAML代码简单地定义了TextBox和TextBlock,并将它们放在它下面,以及两个按钮: BindButton和UnbindButton:
...
...
...
这是相关的C#代码:
public partial class MainWindow : Window
{
TextBox _textBox;
TextBlock _textBlock;
public MainWindow()
{
InitializeComponent();
...
_textBox = this.FindControl("TheTextBox");
_textBlock = this.FindControl("TheTextBlock");
Button bindButton = this.FindControl
绑定是通过调用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.cs、MainWindow.axaml和MainWindow.axaml.cs。ViewModel是一个非常简单的纯非视觉类。这是它的代码:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
// collection of names
public ObservableCollection Names { get; } = new ObservableCollection();
// 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
另请注意,每次Names集合更改时,我们都会触发传递给它们的nameof(NamesCount)和nameof(HasItems)作为参数的PropertyChanged事件。这将通知绑定到那些他们必须更新其目标的属性。
现在看看MainWindow.axaml:
Tom
Jack
Harry
Window DataContext被直接设置为包含一个ViewModel类型的对象,其Names集合填充为Top、Jack和Harry。由于DataContext沿Visual Tree传播,MainWindow.asaml文件中的其余元素将具有相同的DataContext。
ItemControl's Items属性绑定到ViewModel对象的Names集合:
该TextBlock's Text属性绑定到ViewModel
最后,将Button's IsEnabled属性绑定到ViewModel上的HasItems属性,使项目数变为'0',按钮变为禁用状态。
最后,MainWindow.xaml.cs文件仅包含设置事件处理程序以在每次单击按钮时从Names集合中删除最后一项:
public MainWindow()
{
InitializeComponent();
...
Button removeLastItemButton =
this.FindControl("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概念,特别是:
https://www.codeproject.com/Articles/5311995/Multiplatform-Avalonia-NET-Framework-Programming-B