本主题介绍在编写具有适用于 UI 的 XAML 定义的 Windows 运行时应用时可用的依赖属性系统。
什么是依赖属性?
依赖属性是一种专用属性类型。 具体而言,它是一个属性,该属性的值由属于 Windows 运行时的专用属性系统跟踪和影响。
为了支持依赖属性,定义该属性的对象必须是 DependencyObject (换句话说,具有 DependencyObject 基类的类在其继承中的某个位置)。 对于具有 XAML 的 UWP 应用的 UI 定义,许多类型都是 DependencyObject 子类,并且将支持依赖属性。 但是,来自其名称中没有“XAML”的 Windows 运行时命名空间的任何类型都不支持依赖属性;此类类型的属性是没有属性系统的依赖行为的普通属性。
依赖属性的目的是提供一种系统方式,用于基于其他输入(运行应用时在你的应用中发生的其他属性、事件和状态)计算属性的值。 其他输入可能包括:
- 外部输入,例如用户首选项
- 即时属性确定机制,如数据绑定、动画和动画脚本板
- 可重复使用的模板模式,例如资源和样式
- 通过父子关系与对象树中的其他元素已知的值
依赖属性表示或支持编程模型的特定功能,以便使用 XAML for UI 定义 Windows 运行时应用。 这些功能包括:
- 数据绑定
- 风格
- 情节提要动画
- “PropertyChanged”行为;可以实现依赖属性以提供回调,这些回调可将更改传播到其他依赖属性
- 使用来自属性元数据的默认值
- ClearValue 和元数据查找等常规属性系统实用工具
依赖项属性和 Windows 运行时属性
依赖属性通过提供一个全局的内部属性存储来扩展基本的 Windows 运行时属性功能,该存储支持在运行时应用中的所有依赖属性。 这是用于支撑属性的标准模式的替代方案,该模式在属性定义类中有一个私有字段。 可以将此内部属性存储视为任何特定对象存在的一组属性标识符和值(只要它是 DependencyObject)。 存储中的每个属性都由 DependencyProperty 实例标识,而不是按名称标识。 但是,属性系统主要隐藏此实现详细信息:通常可以通过使用简单名称(所使用的代码语言中的编程属性名称或编写 XAML 时的属性名称)来访问依赖属性。
提供依赖属性系统的基类型为 DependencyObject。 DependencyObject 定义可以访问依赖属性的方法,以及 DependencyObject 派生类的实例在内部支持前面提到的属性存储概念。
下面是我们在文档中讨论依赖项属性时使用的术语的总结:
| 术语 | Description |
|---|---|
| 依赖属性 | DependencyProperty 标识符上存在的属性(请参阅下文)。 通常,此标识符可用作定义 DependencyObject 派生类的静态成员。 |
| 依赖属性标识符 | 用于标识属性的常量值,它通常是公共的和只读的。 |
| 属性包装器 | 可调用的 获取 和 设置 Windows 运行时属性的实现。 或者,针对特定语言的原始定义投影。 get 属性包装器实现调用 GetValue,传递相关的依赖属性标识符。 |
属性包装器不仅方便调用方,还向使用 Windows 运行时定义的属性的任何进程、工具或投影公开依赖属性。
以下示例定义为 C# 定义的自定义依赖属性,并显示依赖属性标识符与属性包装之间的关系。
public static readonly DependencyProperty LabelProperty = DependencyProperty.Register(
"Label",
typeof(string),
typeof(ImageWithLabelControl),
new PropertyMetadata(null)
);
public string Label
{
get { return (string)GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
注释
前面的示例不是有关如何创建自定义依赖属性的完整示例。 旨在展示依赖属性的概念,适合喜欢通过代码学习概念的人。 有关此示例的更完整说明,请参阅 自定义依赖项属性。
依赖属性值优先级
获取依赖属性的值时,你将通过参与 Windows 运行时属性系统的任一输入获取为该属性确定的值。 依赖项属性值优先级存在,以便 Windows 运行时属性系统可以以可预测的方式计算值,并且必须熟悉基本优先顺序。 否则,你可能会发现自己正在尝试将属性设置为一个优先级,但系统、第三方调用方或你自己的一些代码在另一个优先级设置此属性,你将因此感到沮丧,因为不知道最终使用的是哪个属性值,以及该值的来源。
例如,样式和模板旨在作为建立属性值的共享起点,从而呈现控件的外观。 但是,在特定控件实例中,你可能希望将其值与常规模板值区分开,例如为该控件提供不同的背景色或不同的文本字符串作为显示内容。 Windows 运行时属性系统将本地值视为优先级高于样式和模板提供的值。 这使得应用特定的值可以覆盖模板,从而使控件更适合在应用程序的用户界面中使用。
依赖属性优先级列表
以下是为依赖属性分配运行时值时属性系统使用的明确顺序。 首先列出了最高优先级。 你会在此列表之后找到更详细的说明。
- 动画值: 活动动画、视觉状态动画或 具有 HoldEnd 行为的动画。 若要产生任何实际效果,应用于属性的动画必须优先于基值(未动画化的值),即使该值是本地设置的也是如此。
- 本地值: 可以通过属性包装器的便利性来设置本地值,这等同于在 XAML 中设置为属性或属性元素,或者通过使用特定实例的属性调用 SetValue 方法。 如果使用绑定或静态资源设置本地值,则这些值将按优先级执行,就像设置了本地值一样,如果设置了新的本地值,则清除绑定或资源引用。
- 模板化属性: 如果元素是模板的一部分创建的(来自 ControlTemplate 或 DataTemplate),则具有这些元素。
- 样式设置器: 来自页面或应用程序资源的样式中的 Setter 的值。
- 默认值: 依赖属性可以将默认值作为其元数据的一部分。
模板化属性
模板化属性作为优先项不适用于直接在 XAML 页标记中声明的元素的任何属性。 模板化属性概念仅适用于在 Windows 运行时向 UI 元素应用 XAML 模板时创建的对象,从而定义其视觉对象。
从控件模板设置的所有属性都具有某种类型的值。 这些值几乎类似于控件的扩展默认值集,通常与稍后可以通过直接设置属性值来重置的值相关联。 因此,模板集值必须与真正的本地值区分开来,以便任何新的本地值都可以覆盖它。
注释
在某些情况下,如果模板未能公开 {TemplateBinding} 标记扩展 引用,则模板甚至可能替代本地值,这些引用应已在实例上设置的属性。 这通常仅在属性实际上不打算在实例上设置时完成,例如,它仅与视觉对象和模板行为相关,而不适用于使用该模板的控件的预期函数或运行时逻辑。
绑定和优先级
绑定操作在其适用的任何范围内都具有适当的优先级。 例如,应用于本地值的 {Binding} 作为本地值运作,而属性设置器的 {TemplateBinding} 标记扩展 的应用则类似于样式设置器的作用。 由于绑定必须等到运行时才能从数据源获取值,因此确定任何属性的属性值优先级的过程也会扩展到运行时。
绑定不仅以与本地值相同的优先级运行,而且它们实际上是本地值,其中绑定是延迟的值的占位符。 如果某个属性值已有绑定,并且在运行时设置了一个本地值,那么该绑定将被完全替换。 同样,如果调用 SetBinding 来定义仅在运行时存在的绑定,则可以替换在 XAML 或以前执行的代码中应用的任何本地值。
分镜头动画和基值
情节提要动画基于基本值的概念。 基值是属性系统使用优先级确定的值,但省略查找动画的最后一步。 例如,基值可能来自控件的模板,也可能来自在控件实例上设置本地值。 无论哪种方式,应用动画都会覆盖此基值,并应用动画值,只要动画继续运行。
对于动画属性,如果动画未显式指定 From 和 To,或者动画在完成后将属性还原为其基值,则基值仍对动画的行为产生影响。 在这些情况下,动画不再运行后,将再次使用优先级的其余部分。
但是,指定具有 HoldEnd 行为的 To 的动画可以重写本地值,直到动画被删除,即使它以视觉方式显示为停止也是如此。 从概念上讲,这就像永远运行的动画,即使 UI 中没有视觉动画也是如此。
多个动画可以应用于单个属性。 这些动画中的每一个都可能被定义为替换源于值优先级中不同层次的基值。 但是,这些动画都将在运行时同时运行,这通常意味着它们必须合并其值,因为每个动画对值的影响都相等。 这取决于动画的具体定义方式,以及所涉及值的类型。
有关详细信息,请参阅 情节提要动画。
默认值
自定义依赖属性主题中更详细地介绍了使用 PropertyMetadata 值建立 依赖属性 的默认值。
即使这些默认值未在该属性的元数据中显式定义,依赖属性仍具有默认值。 除非元数据更改了这些属性,否则 Windows 运行时依赖项属性的默认值通常为下列值之一:
- 使用运行时对象或基本 对象 类型( 引用类型)的属性的默认值为 null。 例如,DataContext 为 null,直到其被主动设置或获得继承为止。
- 使用基本值(如数字或布尔值 类型)的属性使用该值的预期默认值。 例如,0 表示整数和浮点数,布尔值为 false 。
- 使用 Windows 运行时结构的属性具有通过调用该结构的隐式默认构造函数获得的默认值。 此构造函数使用结构的每个基本值字段的默认值。 例如,Point 值的默认初始化是将 X 和 Y 值设为 0。
- 使用枚举的属性具有该枚举中第一个已定义成员的默认值。 检查特定枚举的参考,以查看默认值是什么。
- 使用字符串(适用于 .NET 的 System.String、Platform::String for C++/CX)的属性的默认值为空字符串(“”)。
- 由于本主题中进一步讨论的原因,集合属性通常不作为依赖属性实现。 但是,如果实现自定义集合属性并希望其为依赖属性,请确保避免意外的单例模式,如自定义依赖属性章节末尾附近所述。
依赖属性提供的属性功能
数据绑定
依赖属性可以通过应用数据绑定来设置其值。 数据绑定在 XAML、{x:Bind} 标记扩展或代码中的 Binding 类中使用 {Binding} 标记扩展语法。 对于数据绑定属性,最终属性值确定将延迟到运行时。 此时,该值是从数据源获取的。 依赖属性系统在此处扮演的角色是启用占位符行为,以便在值尚未知时加载 XAML 等作,然后通过与 Windows 运行时数据绑定引擎交互在运行时提供值。
以下示例使用 XAML 中的绑定设置 TextBlock 元素的 Text 值。 绑定使用继承的数据上下文和对象数据源。 (这两个都未显示在缩短的示例中;有关显示上下文和源的更完整的示例,请参阅 数据绑定深度。
<Canvas>
<TextBlock Text="{Binding Team.TeamName}"/>
</Canvas>
还可以使用代码而不是 XAML 建立绑定。 请参阅 SetBinding。
注释
出于依赖属性值优先级的目的,此类绑定被视为本地值。 如果为最初保存 绑定 值的属性设置另一个本地值,则完全覆盖绑定,而不仅仅是绑定的运行时值。 {x:Bind}绑定是使用生成的代码实现的,该代码将设置属性的本地值。 如果为使用 {x:Bind} 的属性设置本地值,则在下次评估绑定时将替换该值,例如当它在其源对象上观察到属性更改时。
绑定源、绑定目标、FrameworkElement 的角色
要成为绑定的源,属性不需要是依赖属性;通常可以将任何属性用作绑定源,但这取决于编程语言,并且每个属性都有特定的边缘情况。 但是,要成为 {Binding} 标记扩展 或 Binding 的目标,该属性必须是依赖属性。 {x:Bind} 没有此要求,因为它使用生成的代码来应用其绑定值。
如果要在代码中创建绑定,请注意 ,SetBinding API 仅为 FrameworkElement 定义。 但是,可以改为使用 BindingOperations 创建绑定定义,从而引用任何 DependencyObject 属性。
对于代码或 XAML,请记住 DataContext 是 FrameworkElement 属性。 通过使用父子属性继承(通常在 XAML 标记中建立)的形式,绑定系统可以解析父元素上存在的 DataContext 。 即使子对象(具有目标属性)不是 FrameworkElement 并因此不持有自己的 DataContext 值,此继承仍然可以评估。 但是,要继承的父元素必须是 FrameworkElement 才能设置并保存 DataContext。 或者,必须定义绑定,使其可以使用 DataContext 的 null 值运行。
设置绑定并不是大多数数据绑定场景中唯一需要的内容。 为了使单向或双向绑定有效,源属性必须支持更改通知,这些通知应能传播到绑定系统并传递给目标。 对于自定义绑定源,这意味着该属性必须是依赖属性,或者该对象必须支持 INotifyPropertyChanged。 集合应支持 INotifyCollectionChanged。 某些类在其实现中支持这些接口,以便它们用作数据绑定方案的基类;此类的示例为 ObservableCollection<T>。 有关数据绑定以及数据绑定与属性系统的关系的详细信息,请参阅 深入的数据绑定。
注释
此处列出的类型支持Microsoft .NET 数据源。 C++/CX 数据源使用不同的接口来更改通知或可观察行为,请参阅 深入的数据绑定。
样式和模板
样式和模板是属性被定义为依赖属性的两种情形。 样式可用于设置定义应用的 UI 的属性。 样式定义为 XAML 中的资源、 资源集合中的 条目或单独的 XAML 文件(如主题资源字典)。 样式与属性系统交互,因为它们包含属性设置器。 此处最重要的属性是控件的 Control.Template 属性:它定义控件的大部分视觉外观和视觉状态。 有关样式的详细信息,以及定义 样式 和使用 setter 的一些示例 XAML,请参阅 样式控件。
来自样式或模板的值是延迟的值,类似于绑定。 这样,控件用户就可以重新模板控件或重新定义样式。 这就是为什么样式中的属性设置器只能作用于依赖属性,而不是普通属性。
情节提要动画
可以使用故事板动画对依赖属性值进行动画处理。 Windows 运行时中的板绘动画不仅是装饰性的视觉效果。 将动画视为状态机技术更有用,它可以设置单个属性的值或控件的所有属性和视觉对象的值,并随时间推移更改这些值。
若要进行动画处理,动画的目标属性必须是依赖属性。 此外,若要进行动画处理,目标属性的值类型必须受现有 时间线派生动画类型之一的支持。 可以使用内插或关键帧技术对 Color、 Double 和 Point 的值进行动画处理。 大多数其他值都可以使用离散 对象 关键帧进行动画处理。
动画应用并运行时,动画值优先于该属性的任何其他值(例如本地值)。 动画还具有一个可选的 HoldEnd 行为,即使动画在视觉上看似已停止,该行为仍可能使动画持续影响属性值。
状态机原则体现在将情节提要动画用作 VisualStateManager 控件的状态模型的一部分。 有关情节提要动画的详细信息,请参阅 情节提要动画。 有关 VisualStateManager 和为控件定义视觉状态的详细信息,请参阅视觉状态的情节提要动画或控件模板。
属性更改行为
属性更改行为是依赖属性术语的“依赖”部分的来源。 当另一个属性可能影响第一个属性的值时,保持属性的有效值在许多框架中是一个困难的开发问题。 在 Windows 运行时属性系统中,每个依赖属性都可以指定每当其属性值更改时调用的回调。 此回调可用于以通常同步的方式通知或更改相关属性值。 许多现有的依赖属性具有属性变化行为。 还可以将类似的回调行为添加到自定义依赖项属性,并实现自己的属性更改回调。 有关示例,请参阅 自定义依赖项属性 。
Windows 10 引入了 RegisterPropertyChangedCallback 方法。 这样,应用程序代码就可以在 DependencyObject 实例上更改指定的依赖属性时注册更改通知。
默认值和 ClearValue
依赖属性可以具有定义为其属性元数据的一部分的默认值。 对于依赖属性,其默认值在首次设置属性后不会变得无关紧要。 每当某些其他确定值优先级消失时,默认值可能会在运行时再次应用。 (下一节讨论了依赖属性值优先级。例如,可以有意删除应用于属性的样式值或动画,但希望该值在执行此作后成为合理的默认值。 依赖属性默认值可以提供此值,而无需将每个属性的值专门设置为额外的步骤。
即使已使用本地值设置了属性,也可以有意地将其设置为默认值。 若要将值重置为默认值并再次启用可能替代默认值而不是本地值的优先级的其他参与对象,请调用 ClearValue 方法(将要清除的属性作为方法参数引用)。 你并不总是希望属性直接使用默认值,但清除本地值并还原为默认值可能会让另一个优先级项起作用,例如使用来自控件模板中样式设定器的值。
DependencyObject 和线程
必须在与 Windows 运行时应用显示的当前窗口关联的 UI 线程上创建所有 DependencyObject 实例。 尽管必须在主 UI 线程上创建每个 DependencyObject ,但可以通过访问 DispatcherQueue 属性,使用来自其他线程的调度程序引用来访问这些对象。 然后,可以调用 TryEnqueue 等方法,并在 UI 线程上的线程限制规则中执行代码。
注释
对于 UWP 应用,访问 Dispatcher 属性。 然后,可以在 CoreDispatcher 对象上调用 RunAsync 等方法,并在 UI 线程上的线程限制规则中执行代码。 有关适用于 Windows 应用 SDK 的 UWP 和 WinUI 之间的差异的详细信息,请参阅 线程功能迁移。
DependencyObject 的线程化方面是相关的,因为它通常意味着只有 UI 线程上运行的代码才能更改甚至读取依赖属性的值。 通常可以在典型的 UI 代码中避免线程处理问题,以便正确使用 异步 模式和后台工作线程。 通常,只有在定义自己的 DependencyObject 类型时,并尝试将其用于不一定适合使用 DependencyObject 的数据源或其他场景时,您才会遇到与 DependencyObject 相关的线程问题。