树形编程






4.93/5 (5投票s)
用于在树上进行编程的通用框架提案
引言
本文将介绍我很久以前提出但尚未实现的一些非常通用的编程概念。我希望这些概念能引起足够的兴趣,从而激励我着手实现它们。
这些想法部分已在 WPF、Xamarin、Angular 和 React 中实现,但它们超越了这三个框架所提供的功能。
尽管这些范式与可视化编程相关,但它们也可应用于非可视化编程,如下文所述。
它们也不受任何特定编程语言的束缚,但目前,我计划首先用 C# 实现它们。此外,本文中的所有示例都用 C# 语言给出。
我也不将这些概念与任何特定的标记语言(如 XAML、JSON、HTML)捆绑用于构建树结构,这些都可以尝试。
此外,我只讨论我想实现什么,而不讨论任何具体的实现方式。例如,Binding
通知概念不与任何特定的实现方式绑定,无论是基于观察者模式(如 WPF 实现的)还是基于检查更改的特殊循环(如 Angular 中的),这都取决于实现者。
组合树
软件工程是一种将复杂对象从更简单的可重用对象中组合出来的艺术。当然,更简单的对象可以(也应该)由更简单、更可重用的对象组成,依此类推。
这种组合在软件上创建了一个树状结构——最复杂的对象(通常是最终程序)是树的根。
下图说明了这种组合
"顶层对象
"由两个对象组成:"Level1 Obj1
"和"Level1 Obj2
"。 "Level1 Obj1
"又由两个对象组成:"Level2 Obj1
"和"Level2 Obj2
"。这形成了以下树状结构
类组合树
当你创建包含其他类对象的类时,类组合树会自然地构建起来。
请看下面的ClassCompositionTest
示例。该示例描述了一个非常简化的保险公司模型。
InsuranceCompany
类包含 CompanyName
属性和 Insurances
属性,后者是 Insurance
对象的集合。Insurance
包含 Person
类型的 Supervisor
属性,InsuranceName
属性和 Subscribers
属性,后者是 Person
类型的对象集合。Person
类只包含 Name
属性。
这建立了一个自然的类包含层次结构:InsuranceCompany
包含 InsuranceName
属性和一个 Insurance
类型的集合。反过来,Insurance
包含 InsuranceSupervisor
和 InsuranceName
属性以及一个 Person
类型的集合。Person
包含一个 Name
属性。
请注意,类树节点可以有两种类型
- 包含可通过该节点内唯一名称访问的对象的节点
- 相似对象集合的节点
这是我们处理的所有树的一个重要特征。此类树的数据可以通过 JSON 表示法很好地表示,其中对象可通过大括号中列出的名称访问,对象集合则列在方括号中。
例如,为上述对象提供一些假想数据,将得到以下 JSON
InsuranceCompany :
{
CompanyName : "Geico",
Insurances :
[
{
InsuranceName : "Great Home Insurance",
InsuranceSupervisor :
{
{ Name: "James Clarc" }
},
Subscribers :
[
{ Name : "Joe Doe" },
{ Name : "Jane Smith" }
]
},
{
InsuranceName : "Not So Great Home Insurance",
InsuranceSupervisor :
{
{ Name: "Tom Jefferson" }
},
Subscribers :
[
{ Name : "Sam Adams" },
{ Name : "John Buchanan" }
]
}
]
}
您还可以使用 JSON 来反映树的形状(骨架),而无需列出任何数据
InsuranceCompany :
{
CompanyName : string,
Insurances :
[
{
InsuranceName : string
InsuranceSupervisor : { Name : string }
Subscribers :
[
{ Name : string }
]
}
]
}
这是骨架树的图片表示
请注意,包含命名对象的节点用与命名属性对应的线条表示,这些线条从相同的(节点)顶点开始
而集合节点则用一条水平线段表示,该线段引出垂直线
数据树
类组合树的一个特例是数据树,其中每个类只表示数据。我们上一节中的树实际上是纯数据树——因为上面介绍的类只有属性而没有其他功能。
HTML DOM
HTML DOM是组合树的一个很好的例子,其中每个HTML元素(根节点除外)都位于另一个元素(其父元素)内部。与HTML元素对应的HTML可视化遵循相同的包含层次结构——与父元素对应的可视化包含与子HTML节点对应的可视化。
HTML树中集合节点的例子是列表中的项,或者表格中行中的表格行,或者表格行中的表格列。
React 和 Angular 虚拟 DOM
React 和 Angular 虚拟 DOM 是组合的另一个例子。这些是简化的树,它们使用一些外部数据(React 的情况是属性值,Angular 的情况是包含类的G数据)来生成真实的 HTML DOM。
React 和 Angular 是树转换的绝佳范例,其中原始的更简单的树被转换为更复杂的树。
上图展示了 React 虚拟 DOM 转换为 HTML DOM(使用 React 框架)并最终转换为浏览器视觉效果(使用浏览器功能)的过程。
虚拟到真实 DOM 的生成是通过扩展一些虚拟 DOM 节点完成的——扩展后的节点可以变成更加复杂的节点
WPF 逻辑树和可视化树
树和树转换的最佳、最强大例子之一是 WPF 逻辑树和可视化树。
在 WPF 中,有两种方式可以编程扩展逻辑树节点——使用 ControlTemplate
s(这更类似于 React)和使用 DataTemplate
s(这更类似于 Angular)。
ControlTemplate
扩展的示例在 ControlTemplateExample
解决方案中给出。运行项目时,您将看到以下内容
这是相关的XAML代码(逻辑树)
<Grid>
<Grid.Resources>
<ControlTemplate x:Key="LabeledTextInputControlTemplate"
TargetType="local:LabeledTextInput">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{TemplateBinding Label}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,5,0" />
<TextBox HorizontalAlignment="Stretch"
Grid.Column="1"
Text="{Binding Path=Text,
RelativeSource={RelativeSource Mode=TemplatedParent},
Mode=TwoWay}" />
</Grid>
</ControlTemplate>
</Grid.Resources>
<local:LabeledTextInput Label="Enter Text:"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Height="30"
Margin="10"
Template="{StaticResource LabeledTextInputControlTemplate}"/>
</Grid>
WPF 框架使用其控件模板扩展 LabeledTextInput
控件;它也扩展 TextBox
。我们可以可视化生成的视觉树,例如,使用 Snoop。
LabeledTextInput
是视觉树的一部分,但它包含许多子节点。
运行 DataTemplateExample
解决方案将生成完全相同的视觉效果。不同之处在于,这里我们使用 DataTemplate
而不是 ControlTemplate
。DataTemplate
描述了如何从 ViewModel
类表示的非视觉数据创建视觉树节点。这是 ViewModel
代码
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string Label { get; set; }
#region Text Property
private string _text;
public string Text
{
get
{
return this._text;
}
set
{
if (this._text == value)
{
return;
}
this._text = value;
this.OnPropertyChanged(nameof(Text));
}
}
#endregion Text Property
}
您可以看到此类别有两个属性:Label
和 Text
。Text
属性是可通知的(其更新将触发 INotifyPropertyChanged.PropertyChagned
事件),而 Label
属性则假定不会改变。
这是相关的 XAML 代码
<Grid>
<Grid.Resources>
<local:ViewModel x:Key="TheViewModel"
Label="Enter Text:"/>
<DataTemplate x:Key="ViewModelDataTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Label}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="0,0,5,0" />
<TextBox HorizontalAlignment="Stretch"
Grid.Column="1"
Text="{Binding Path=Text,
Mode=TwoWay}" />
</Grid>
</DataTemplate>
</Grid.Resources>
<ContentControl ContentTemplate="{StaticResource ViewModelDataTemplate}"
Content="{StaticResource TheViewModel}"
VerticalAlignment="Center"
Margin="10"/>
</Grid>
ContentControl
是一个特殊的 WPF 控件,它将非视觉(视图模型)对象(包含在 Content
属性中)与 DataTemplate
(包含在 ContentTemplate
属性中)结合起来。视图模型和 DataTemplate
都作为包含 ContentControl
的 Grid
面板的资源提供。
Xamarin 中的树
Xamarin 提供自己的树对象,这些对象在不同平台上转换为不同的视觉效果。Xamarin for WPF 将 Xamarin 树转换为 WPF 视觉树。
Xamarin 在其他平台上将 Xamarin 树转换为该平台上的视觉元素树。
各种框架中树的比较
附加属性
WPF 和 Xamarin 提供了特殊属性,这些属性可以在使用它们的类之外实现,称为附加属性。它们对于一些事情非常有用,包括后面将描述的复杂对象组合。
附加属性允许通过创建对象到属性值的Map
(Dictionary
)来将属性值附加到任何对象。它们比普通属性更笨重、更慢,但对于以下情况非常有用
- 稀疏表示——如果对象只包含附加属性的默认值,则无需额外内存。
- 属性通知——附加属性允许在每次属性修改时提供回调。
- 附加属性可用于将值附加到对象,而无需修改对象的类型。
- 附加属性可以作为绑定的目标。
树上的绑定
React、Angular、Xamarin 和 WPF 都利用了绑定概念。绑定是同步树上两个属性值的一种方式。
WPF 在上述四个框架中拥有最强大的绑定功能——它的绑定具有 RelativeSource
参数,允许它绑定到树上更高层元素的一个属性路径。其他框架都没有提供类似的功能。尤其是在 Xamarin 方面,这令人失望——它有一个带有 .NET 事件的 .NET 树结构,但未能实现 RelativeSource
。
WPF 还具有 MultiBinding
,允许将绑定目标绑定到多个源,以便每当其中任何一个源发生变化时,目标都会更新。
其他框架不直接提供 MultiBinding
,尽管在 Angular 中也可以通过一些技巧实现类似 MultiBinding
的功能。
树上的事件
WPF 有一个特殊事件的概念,称为路由事件,它们在可视化树中向上(冒泡)和向下(隧道)传播。在 HTML 中,有一些预定义的事件可以冒泡到 HTML DOM。然而,WPF 事件功能强大得多,原因如下:
- 它们也可以隧道式传播——从树根传播到当前元素——这是 HTML 事件无法做到的。
- 可以创建自定义的冒泡或隧道事件——而在 HTML 中,唯一可以冒泡的是预定义事件。
- 可以在 WPF 中创建附加的冒泡或隧道事件——在 WPF 节点类型之外定义的事件,而其他框架没有提供任何现成的方法来实现。
遗憾的是,当前版本的 Xamarin 也不支持事件冒泡或隧道传播。
框架的通用性
WPF是上述框架中最通用的一个。它具有最少数量的基本对象,任何其他对象都可以由这些基本对象构建。WPF按钮可以像最复杂的WPF页面一样,由基本对象组成。WPF使用DirectX将WPF视觉树转换为屏幕上的真实视觉效果。
其他框架无法提供这种程度的通用性。虽然 Angular 和 React 在一定程度上受限于 HTML5 的能力,但 Xamarin 在提供更多基本对象来组合所有框架中的一切方面可能会做得更好。Xamarin 按钮在 Mac 电脑或平板电脑上变成 Mac 按钮,而不是由面板、边框和图标等更精细的元素组成。
由于我对 Mac/iPhone 和 Linux/Android UI 包的了解有限,我不知道这些平台上是否还有更基本的包可以从更基本的元素和基本事件中创建 UI 元素(按钮、下拉菜单、复选框等)的视觉效果,但我相信应该有。一旦视觉效果创建完成,就可以通过基本树来连接行为和事件。
文章主要思想
如上所述,在所有列出的树处理框架中,WPF 是最强大的一个。然而,使用 WPF 有两个缺点
- 微软对 WPF 不再重视,停止了推广。
- WPF 仅适用于 .NET 平台。Silverlight 曾经在其他平台上提供类似(但功能显著较弱)的功能,但微软实际上扼杀了 Silverlight。
本文的主要思想(尚未实现)包括以下步骤
- 创建一个强大的框架,能够处理和转换通用树上的信息。该框架将提供上述所有强大的范式,包括绑定、带树传播的附加事件、附加属性。
- 创建一套最小的原始视觉元素,如边框、面板、图标、图像等。
- 用这些原始元素创建有用的控件(
Button
、CheckBox
等)。 - 将这些原始视觉元素映射到任何平台上相应的视觉效果。一旦原始元素正确映射,复杂的控件也应该能够工作。
在本文中,我将主要集中在第一点——为提供比 WPF 更强大树操作能力的框架创建需求。
我计划首先用 C# 构建第一个树包,之后,我希望也能将其移植到 JavaScript,也许还有 Java。
我再次强调,本文不会描述功能的实现,它只会描述功能的需求。实现方式仍可由实现者个人或团队决定。例如,绑定通知可以通过观察者模式(例如 C# 事件)实现,也可以通过检查更改的循环来实现,就像 Angular 中那样。
附加属性
附加属性几乎可以在任何语言中轻松实现。非 WPF C# 实现的一个示例可在WPF 概念的纯 C# 实现 - 第 1 部分 AProps 和绑定简介中找到。
树上的绑定
什么是绑定
我们首先讨论什么是Binding
,以及为什么Binding
优于观察者模式(例如,C#事件)。
源绑定对象通常通过一些绑定参数相对于目标绑定对象进行选择。特别是在 WPF 中,可以通过以下方式选择
- 默认情况下,它设置为包含在目标元素的特殊
DataContext
属性中。 - 如果指定了
Binding.Source
属性,则源元素被选为该对象。 - 如果指定了
Binding.ElementName
,则源对象被选为 XAML 中名称的元素。 - 如果指定了
Binding.RelativeSource
,则绑定的源对象将通过各种参数(例如,按源对象类型)在可视化树中查找。
WPF 是我所知的唯一一个允许使用 RelativeSource
属性在树中选择绑定源对象的框架。其他框架在绑定时根本不考虑树结构。
现在我将回答绑定在哪些方面比使用观察者(C# 事件)进行属性更改通知更强大。在以下条件下,绑定将触发并更改目标值:
- 在源和目标属性的路径以及树结构(如果需要)存在的情况下,绑定在对象上创建并设置。
- 在绑定存在的情况下,路径和树结构被创建。
- 在绑定以及源和目标路径以及树结构(如果需要)存在的情况下,源属性发生更改。
前两个条件确保绑定应在初始化阶段触发并更改所需的属性值。最后一个条件是最常见的——当源更改时,目标应被通知并更改。
观察者(或 C# 事件)只适用于最后一个条件。观察者不会在初始化阶段触发。这个恼人的缺点应该为大多数使用 C# 事件的开发人员所熟悉。如果事件用于同步多个属性值,则在创建包含这些事件的两个对象或添加事件处理程序时,还必须处理初始化。
绑定会自动处理初始化。在这方面,绑定比更改通知事件更有用、更强大的范例。
ObservableCollection 和集合绑定
在 C# 中,ObservableCollection<T>
是一个实现了 ICollectionChanged
接口的集合,该接口包含 CollectionChanged
事件。使用此事件,可以获取添加到或从集合中移除的项的枚举,从而保持集合内项的信息最新。
当 ItemsControl.ItemsSource
属性绑定到包含 ObservableCollection<T>
的属性时,WPF 中会使用集合绑定。当属性更改为不同的集合时,以及当集合本身更改时(其项目被添加或删除),ItemsControl
的视觉效果将更新。
当前框架绑定限制及改进方法
如上所述,WPF 仍然是我所知并在此列出的所有框架中拥有最强大的绑定机制(尽管 Angular、React 和 Xamarin 框架是在 WPF 之后很久才创建的)。事实上,WPF 是唯一一个其绑定利用其元素以树结构排列这一事实的框架(通过 Binding.RelativeSource
属性)。
尽管如此,WPF 在绑定方面仍有许多限制,理想的框架可以解决这些问题。以下是这些限制及其建议的修复方案。
- 在 WPF 中,只有定义在
DependencyObject
上的DependencyProperty
才能作为绑定的目标。这个要求是人为的,可以移除,正如我在深度解析无 WPF 属性绑定 - 单向属性绑定中所示。 - 在 WPF 中,
Binding
源属性可以指定复杂路径,而target
属性只能直接在目标上定义。事实上,源和目标可以完全对称地处理,并且很容易创建允许目标端也具有复杂路径的绑定(如在深入了解无 WPF 属性绑定 - 单向属性绑定中所示)。 - 单向绑定与双向绑定——WPF、Xamarin 和 Angular 允许双向绑定,而 React 只允许单向绑定。我从未遇到过需要同时进行双向绑定的应用程序。通常,当用户修改
TextBox.Text
属性时,它不需要同时被来自后端的数据修改,反之亦然。这让我思考了以下关于绑定方向的提议——始终保持绑定为单向,并使其易于更改在不同时间可能需要不同方向的某些绑定的方向。 - 改进绑定的另一种方法是允许以更精细的方式选择绑定的源。例如,
RelativeSource
在可视化树中搜索绑定源时,除了按Type
过滤候选对象外,还可以根据各种属性值进行过滤。
树上的变更通知和属性与集合绑定的泛化
在本小节中,我计划描述绑定概念的进一步泛化。
我们从一个与文章开头非常相似的示例开始
InsuranceCompany :
{
CompanyName : "Geico",
Insurances :
[
{
InsuranceName : "Great Home Insurance",
InsuranceSupervisor :
{
{ Name: "James Clarc" }
},
Subscribers :
[
{
Name : "Joe Doe",
YearlyProfit : $600
},
{
Name : "Jane Smith",
YearlyProfit : $400
}
]
},
{
InsuranceName : "Not So Great Home Insurance",
InsuranceSupervisor :
{
{ Name: "Tom Jefferson" }
},
Subscribers :
[
{
Name : "Sam Adams",
YearlyProfit : $100
},
{
Name : "John Buchanan",
YearlyProfit : $200
}
]
}
]
}
目前唯一的区别是,我们为每个 Subscriber
添加了 YearlyProfit
字段。
这是树的骨架
InsuranceCompany :
{
CompanyName : string,
Insurances :
[
{
InsuranceName : string
InsuranceSupervisor : { Name : string }
Subscribers :
[
{
Name : string,
YearlyProfit : number
}
]
}
]
}
假设我们想在保险级别和顶层(公司级别)添加 YearlyProfit
字段,使其成为该保险或整个公司下所有订阅者级别的 YearlyProfit
值的总和。当订阅者级别的年利润发生变化,或者当订阅者被添加或删除时,聚合级别的值应该随之变化。
我们应该能够创建一个保险级别的绑定,指定源和目标路径,以及一个聚合转换器
InsuranceYearlyProfitBinding :
{
SourcePath : "Subscribers/YearlyProfit",
TargetPath : "YearlyProfit",
Converter : "typeof(SumAggregationConverter)"
}
公司层面的来源将更长
CompanyYearlyProfitBinding :
{
SourcePath : "Insurances/Subscribers/YearlyProfit",
TargetPath : "YearlyProfit",
Converter : "typeof(SumAggregationConverter)"
}
还可以遇到这样的情况,即并非所有订阅者都应被计算在内,而只有符合特定条件的订阅者才应被计算。这可以在绑定中添加一个 Where
子句。
还可以想到从树中更深层的节点到更上层节点的绑定连接(类似于 RelativeSource
WPF 绑定,但可能更复杂)。
树转换
转换类型
树转换允许从一个较不复杂的树创建一个更复杂的树。
我上面给出了一些树转换的例子——特别是虚拟 DOM 转换为真实 HTML DOM 以及 WPF 逻辑树到视觉树的转换。
通常,在任何转换下,某些节点会被扩展成一个完整的节点层次结构,而树的其余结构保持不变。
WPF 的逻辑树节点扩展为视觉树节点有两种方式:
- 通过
ControlTemplate
s - 通过
DataTemplate
s
ControlTemplate
扩展导致节点的递归组合,而 DataTemplate
扩展将非视觉数据转换为视觉层次结构。
ControlTemplate
转换与 React 中创建组件非常相似。我将这种转换方式称为“组件节点扩展”。在接下来的子节中,我将更详细地描述它。
组件节点扩展
假设您有许多各种树节点(例如,WPF 元素或 React 组件),您希望它们协同工作(并且,在视觉节点的情况下,也位于一起)。
您可以将它们组合到自己的组件子树中,并创建一个与该组件子树对应的组件节点,以便树中的每个组件节点都将被组件子树替换。
上图展示了一个非常简单的组件 LabeledTextComponent
,其组件子树由一个根节点和两个命名节点组成——LabelComponent
和 TextInputComponent
。如果您了解 WPF,可以想象一个由 TextBlock
和 TextBox
组成的控件——TextBlock
将包含字段名称,而 TextBox
将包含字段值。
遵循这个 WPF 示例,在逻辑树转换为视觉树的过程中,逻辑树中 LabeledTextComponent
的所有实例都将被扩展为完整的组件子树。
在 WPF 中,组件子树由组件(WPF 控件)的 ControlTemplate
决定。在 React 中,组件子树通常由嵌入的类似 HTML 的 JSX 代码决定。
请注意,组件-部分关系可以是递归的,即复合组件可以成为更大组件的一部分。
组件子树中的元素可以通过各种方式连接——例如,通过它们的属性绑定,或者通过代码隐藏,或者通过 WPF 控件触发器——这超出了本文的范围。
为了使组件可定制,它们应该具有一些定制参数(属性)。例如,我们简单的 LabeledTextComponent
应该至少有两个属性
Label
- 包含文本字段的名称Value
- 包含文本字段的值
这些属性应通过绑定连接到子组件中的相应属性——在我们的例子中,LabelComponent
的 Label
属性应绑定到 LabelTextComponent
的 Label
属性,而 TextInputComponent
的 Text
属性应绑定到 LabeledTextComponent
的 Text
属性。
在 WPF 中,这通过将 RelativeSource
设置为 TemplatedParent
模式的绑定来实现。在 React 中,这通过包含超组件各种属性名称和值的 this.props
类成员来实现。
在 WPF 中,同一个 Control
(Component
)可以有各种 ControlTemplate
实现,这非常灵活,但也可能导致以下问题。假设您想控制一个子组件的某个属性,而该属性在原始 Control
中不存在。最常见的 WPF 示例是控制 Border
的 CornerRadius
属性,例如 Button
没有 CornerRadius
。您当然可以派生一个 Button
类,创建一个例如 ButtonWithCornerRadius
类。但这样一来,您就必须为原始 Button
类没有的任何属性组合进一步派生它。此外,如果您想为已存在的代码中的每个 Button
提供圆角,您就必须将所有这些实例替换为新的 ButtonWithCornerRadius
类。
这就是附加属性非常方便的地方。您可以在 Button
类外部创建一个 CornerRadius
属性,然后在类内部使用它,就像它是类的属性一样。您还可以将 ControlTemplate
Border
的 CornerRadius
属性绑定到 Button
上的附加属性。
ButtonCornerRadius
示例展示了如何在不继承 Button
类的情况下为 WPF Button
附加一个新的 CornerRadius
属性。运行它时,您会看到一个带有 10px CornerRadius
的蓝色“Button
”。
这是相关的 XAML 代码
<Button local:AttachedProps.CornerRadius="10"
Width="100"
Height="50"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="Blue"
CornerRadius="{TemplateBinding local:AttachedProps.CornerRadius}" />
</ControlTemplate>
</Button.Template>
</Button>
行 local:AttachedProps.CornerRadius="10"
将 Button
上的附加属性 CornerRadius
设置为 10px,而行 CornerRadius="{TemplateBinding local:AttachedProps.CornerRadius}"
将 ControlTemplate
中 Border
的 CornerRadius
绑定到 Button
的附加属性。
这种组件-部分绑定也可以泛化并应用于 WPF 和 React 之外。
树上传播的事件
最后,我将花几段文字谈谈在树上传播的事件。这种事件在 WPF 中被称为路由事件。
正如我在通用(非 WPF)树到 LINQ 和树上传播的事件中所示,在 WPF 之外实现冒泡和隧道事件很容易。
此类事件可以在树节点处触发,并在不同的树节点处处理(例如,在事件触发节点的祖先节点之一)。
我想提醒您为什么这些事件很重要。当您将任何组件由基本元素组成时(例如,由边框、图标和文本组成一个按钮),事件通常在底部元素触发,例如,MouseDown
事件可能在按钮的边框上触发,而不是在按钮本身上。重要的是,此类事件会沿树向上传播到按钮本身,在那里可以附加此类事件的处理程序。
结论
在本文中,我阐述并概括了来自 WPF、Xamarin、Angular 和 React 框架的概念。
本次讨论的目的是为建立一个通用的、与平台无关的框架奠定基础,该框架将比上述任何框架都具有更强大的组合能力,同时可用于在各种平台上创建 GUI。