浅析依赖属性(DependencyProperty)
在WPF中,引入了依赖属性这个概念,提到依赖属性时通常都会说依赖属性能节省实例对内存的开销。此外依赖属性还有两大优势。
- 支持多属性值,依赖属性系统可以储存多个值,配合Expression、Style、Animation等可以给我们带来很强的开发体验。
- 加入了属性变化通知,限制、验证等功能。方便我们使用少量代码实现以前不太容易实现的功能。
本文将主要介绍依赖属性是如何存取数据的以及多属性值的取值优先级。
CLR属性
CLR属性是private字段安全访问的封装
对象实例的每个private字段都会占用一定的内存,字段被CLR属性封装起来,每个实例看上去都带有相同的属性,但并不是每个实例的CLR属性都会多占一点内存。因为CLR属性是一个语法糖,本质是Get/Set方法,再多的实例方法也只有一个拷贝。
以TextBlock为例,共有107个属性,但通常使用的最多的属性是Text,FontSize,FontFamily,Foreground这几个属性,大概有100个左右属性是没有使用的。若按照CLR属性分配空间,假设每个属性都封装了一个4Byte的字段,一个5列1000行的列表浪费的空间就是4×100×5×1000≈1.9M。而依赖属性则是省下这些没有用到的属性所需的空间,其关键就在于依赖属性的声明和使用。
依赖属性的声明和使用
依赖属性的使用很简单,只需要以下几个步骤就可以实现:
- 让所在类型直接或间接继承自
DependecyObject
。在WPF中,几乎所有的控件都间接继承自DependecyObject
。 - 声明一个静态只读的
DependencyProperty
类型变量,这个静态变量所引用的实例并不是通过new操作符创建,而是使用简单的单例模式通过DependencyProperty.Register
创建的,下文会对这个方法进行介绍。 - 使用依赖属性的实例化包装属性读写依赖属性。
按照以上步骤可以写出如下代码:
public class ValidationParams:DependencyObject
{
public object Param1
{
get { return (object)GetValue(Param1Property); }
set { SetValue(Param1Property, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty Param1Property =
DependencyProperty.Register("Param1", typeof(object), typeof(ValidationParams), new PropertyMetadata(null));
}
代码中Param1Property
才是真正的依赖属性,Param1
是依赖属性的包装器,这里有一个命名约定,依赖属性的名称是对应包装器名称+Property
组成。在Visual studio中输入propdp
,然后Tab
键就会自动生成依赖属性以及包装器的代码片段,然后根据实际情况修改相应的参数和类型。
Register
方法的第一个参数为string类型,用来指明作为依赖属性包装器的CLR属性;第二个参数指定依赖属性存储什么类型的值,第三个参数指明依赖属性的宿主是什么类型,第四个参数是依赖属性元数据,包含默认值,PropertyChangedCallback,CoerceValueCallback,ValidateValueCallback等委托。
依赖属性存取值的机制
从修饰符可以看出依赖属性是一个静态的只读变量,要确保不同实例的依赖属性正确赋值,肯定不能把数据直接保存到这个静态变量中。这里其实也是依赖属性机制的核心。
与依赖属性存取数据有三个关键的类型:DependencyProperty
、DependencyObject
、EffectiveValueEntry
。
DependencyProperty
:依赖属性实例都是单例,其中DefaultMetadata
存储了依赖属性的默认值,提供变化通知、限制、检验等回调以及子类override依赖属性的渠道。GlobalIndex
用于检索DependencyProperty
的实例。应用程序中注册的所有DependencyProperty
的实例都存放于名为PropertyFromName
的Hashtable中。DependencyObject
:依赖属性的宿主对象,_effectiveValues
是一个私有的有序数组,用来存储本对象实例中修改过值得依赖属性,GetValue
、SetValue
方法用于读写依赖属性的数值。EffectiveValueEntry
:存储依赖属性真实数值的对象。它可以实现多属性值,具体来说就是内部可以存放多个值,根据当前的状态确定对外暴露哪一个值(这里涉及到多个值选取的优先顺序的问题)。
前边提到依赖属性实例是使用简单的单例模式通过DependencyProperty.Register
创建的。通过阅读源码发现,所有的DependencyProperty.Register
方法重载都是对DependencyProperty.RegisterCommon
的调用。为了方便介绍,下文只是提取RegisterCommon
方法中的关键代码
private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback)
{
FromNameKey key = new FromNameKey(name, ownerType);
.....略去校验以及默认元数据代码
// Create property
DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
// Map owner type to this property
// Build key
lock (Synchronized)
{
PropertyFromName[key] = dp;
}
return dp;
}
代码的大致意思是生成一个FromNameKey
类型的key,然后构造一个DependencyProperty
实例dp
,并存放到名为PropertyFromName
的Hashtable中,最后返回这个实例dp
。
FromNameKey
是DependencyProperty
中的内部私有类,其代码如下:
private class FromNameKey
{
public FromNameKey(string name, Type ownerType)
{
_name = name;
_ownerType = ownerType;
_hashCode = _name.GetHashCode() ^ _ownerType.GetHashCode();
}
public override int GetHashCode()
{
return _hashCode;
}
...略去部分代码
private string _name;
private Type _ownerType;
private int _hashCode;
}
这里特地介绍这个类是因为FromNameKey
对象是依赖属性实例的key,它的hashcode是由Register
的第一个参数(依赖属性包装器属性名称字符串)的hashcode和第三个参数(依赖属性宿主类型)的hashcode做异或运算得来的,这样设计确保了每个DependecyObject
类型中不同名称的依赖属性的实例是唯一的。
接下来就是使用(读写)依赖属性了,前边提到DependecyObject
中提供了GetValue
、SetValue
方法用于读写依赖属性。先看下GetValue
方法,代码如下:
public object GetValue(DependencyProperty dp)
{
// Do not allow foreign threads access.
// (This is a noop if this object is not assigned to a Dispatcher.)
//
this.VerifyAccess();
ArgumentNullException.ThrowIfNull(dp);
// Call Forwarded
return GetValueEntry(
LookupEntry(dp.GlobalIndex),
dp,
null,
RequestFlags.FullyResolved).Value;
}
方法前几行是线程安全性和参数有效性检测,最后一行是获取依赖属性的值。LookupEntry
是根据DependencyProperty
实例的GlobalIndex
在_effectiveValues
数组中查找依赖属性的有效值EffectiveValueEntry
,找到后返回其索引对象EntryIndex
。EntryIndex
主要包含Index
和Found
两个属性,Index
表示查找到的索引值,Found
表示是否找到目标元素。
GetValueEntry
根据LookupEntry
方法返回的EntryIndex
实例查找有效值EffectiveValueEntry
。如果entryIndex.Found
为true,则根据Index
返回_effectiveValues
中的元素,否则new一个EffectiveValueEntry
实例。
SetValue
方法也是先通过GetValueEntry
查找有效值对象,找到则修改旧数据,反之则new一个EffectiveValueEntry
实例赋值,并添加到_effectiveValues
中。
至此,我们也大致了解了依赖属性存取值的秘密。DependencyProperty
并不保存实际数值,而是通过其GlobalIndex
属性来检索属性值。每一个DependencyObject
对象实例都有一个EffectiveValueEntry
数组,保存着已赋值的依赖属性的数据,当要读取某个依赖属性的值时,会在这个数组中去检索,如果没有检索到,会从DependencyProperty
保存的DefaultMetadata中读取默认值(这里只是简单的描述这个过程,真实情况还涉及到元素的style、Theme、父节点的值等)。
依赖属性值的优先级
前边提到依赖属性支持多属性值,WPF中可以通过多种方法为一个依赖项属性赋值,如通过样式、模板、触发器、动画等为依赖项属性赋值的同时,控件本身的声明也为属性进行了赋值。在这种情况下,WPF只能选择其中的一种赋值作为该属性的取值,这就涉及到取值的优先级问题。
从上一小节的图中可以看到EffectiveValueEntry
中有两个属性:ModifiedValue
和BaseValueSourceInternal
,ModifiedValue
用于跟踪依赖属性的值是否被修改以及被修改的状态。BaseValueSourceInternal
是一个枚举,它用于表示依赖属性的值是从哪里获取的。在与ModifiedValue
一起使用,可以确定最终呈现的属性值。
EffectiveValueEntry
中GetFlattenedEntry
方法中以下代码及注释可以看出强制值>动画值>表达式值这样得优先级
internal EffectiveValueEntry GetFlattenedEntry(RequestFlags requests)
{
......略去部分代码
// Note that the modified values have an order of precedence
// 1. Coerced Value (including Current value)
// 2. Animated Value
// 3. Expression Value
// Also note that we support any arbitrary combinations of these
// modifiers and will yet the precedence metioned above.
if (IsCoerced)
{
......略去部分代码
}
else if (IsAnimated)
{
......略去部分代码
}
else
{
......略去部分代码
}
return entry;
}
其中表达式值包含样式、模板、触发器、主题、控件本身对属性赋值或者绑定表达式。其优先级则是在BaseValueSourceInternal
中定义的。枚举元素排列顺序与取值优先级顺序刚好相反。
// Note that these enum values are arranged in the reverse order of
// precendence for these sources. Local value has highest
// precedence and Default value has the least. Note that we do not
// store default values in the _effectiveValues cache unless it is
// being coerced/animated.
[FriendAccessAllowed] // Built into Base, also used by Core & Framework.
internal enum BaseValueSourceInternal : short
{
Unknown = 0,
Default = 1,
Inherited = 2,
ThemeStyle = 3,
ThemeStyleTrigger = 4,
Style = 5,
TemplateTrigger = 6,
StyleTrigger = 7,
ImplicitReference = 8,
ParentTemplate = 9,
ParentTemplateTrigger = 10,
Local = 11,
}
综合起来依赖属性取值优先级列表如下:
- 强制:在
CoerceValueCallback
对依赖属性约束的强制值。 - 活动动画或具有Hold行为的动画。
- 本地值:通过CLR包装器调用
SetValue
设置的值,或者XAML中直接对元素本身设置值(包括binding
、StaticResource
、DynamicResource
) - TemplatedParent模板的触发器
- TemplatedParent模板中设置的值
- 隐式样式
- 样式触发器
- 模板触发器
- 样式
- 主题样式的触发器
- 主题样式
- 继承。这里的继承Inherited是xaml树中的父元素,要区别于面向对象语言子类继承(derived,译为派生更合适)与父类
- 依赖属性元数据中的默认值
WPF对依赖属性的优先级支持分别使用了ModifiedValue
和BaseValueSourceInternal
,大概是因为约束强制值和动画值是临时性修改,希望在更改结束后能够恢复依赖属性原有值。而对于样式、模板、触发器、主题这些来说相对固定,不需要像动画那样结束后恢复原来的值。
总结
依赖属性是WPF中非常核心的一个概念,涉及的知识点也非常多。像RegisterReadOnly
、PropertyMetadata
、OverrideMetadata
、AddOwner
都能展开很多内容。要想真正掌握依赖属性,这些都是需要了解的。
浅析依赖属性(DependencyProperty)的更多相关文章
- WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性
原文:WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性 如果你要自定义一个图片按钮控件,那么如何在主窗体绑定这个控件上图片的Source呢? ...
- WPF依赖属性DependencyProperty
写在之前: 依赖属性算是WPF醉醉基础的一个组成了.平时写代码的时候,简单的绑定很轻松,但是遇到复杂的层次比较多的绑定,真的是要命.所以,我觉得深刻认识依赖属性是很有必要的.本篇只是个人学习的记录,学 ...
- [Silverlight]监听指定控件(FrameworkElement)的依赖属性(DependencyProperty)的更改
前言 转载请注明出处:http://www.cnblogs.com/ainijiutian 最近在silverlight项目使用Telerik的控件,遇到一个问题.就是使用RadBusyIndicat ...
- WPF 依赖属性源码 洞察微软如何实现DependencyProperty
依赖属性DependencyProperty是wpf最重要的一个类,理解该类如何实现对学习wpf帮助很大! 终于找到了该类的源码!仔细阅读源码,看看微软如何玩的花招! File: Base\Syste ...
- windows phone之依赖属性(DependencyProperty)
Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行时 (CLR) 属性的功能,这些服务通常统称为 WPF 属性系统.由 WPF ...
- [转]WPF的依赖属性是怎么节约内存的
WPF升级了CLR的属性系统,加入了依赖属性和附加属性.依赖属性的使用有很多好处,其中有两点是我认为最为亮眼的: 1)节省内存的开销; 2)属性值可以通过Binding依赖于其它对象上,这就使得我的数 ...
- 2000条你应知的WPF小姿势 基础篇<57-62 依赖属性进阶>
在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000ThingsYou Should Know About C# 和 2,00 ...
- 2000条你应知的WPF小姿势 基础篇<51-56 依赖属性>
前一阵子由于个人生活原因,具体见上一篇,耽搁了一阵子,在这里也十分感谢大家支持和鼓励.现在开始继续做WPF2000系列. 在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件 ...
- WPF 依赖属性
依赖属性,简单的说,在WPF控件应用过程中,界面上直接可以引用的属性 如:<Button Content="aaa"></Button> Content称为 ...
- WPF整理-为User Control添加依赖属性
依赖属性 ".NET properties are nothing more than syntactic sugar over set and get methods." 我们知 ...
随机推荐
- 【python基础】类-继承
编写类时,并非总是要从空白开始.如果要编写的类时另一个现成类的特殊版本,可使用继承.一个类继承另一个类时,它将自动获得另一个类的所有属性和方法 原有的类称为父类,而新类被称为子类.子类继承了其父类的所 ...
- Centos7中搭建Redis6集群操作步骤
目录 下载安装包 解压安装装包 安装依赖 安装 创建目录 设置配置文件 创建启动服务 制作启动文件 启动并验证Redis 开放防火墙端口 创建集群 集群其他操作 注意 下载安装包 # 进入软件下载目录 ...
- 【SpringBoot】条件装配 @profile
profile 使用说明: @profile注解的作用是指定类或方法在特定的 Profile 环境生效,任何@Component或@Configuration注解的类都可以使用@Profile注解. ...
- 1.简述Hibernate的工作原理。
(1).首先,Configuration读取Hibernate的配置文件和映射文件中的信息,即加载配置文件和映射文件,并通过Hibernate配置文件生成一个多线程的SessionFactory对象: ...
- JDK源码-StringJoiner源码分析
背景 功能描述:将多个元素使用指定符号前后连接为字符串:eg:1 2 3 4 5 , => 1,2,3,4,5 要点: 多个元素 指定分隔符 分隔符只在元素之间,不能作为第一或最后一个 使用方法 ...
- 语音合成技术汇总1:Glow-TTS:通过单调对齐实现文本到语音的生成流
今天开始开一期语音合成经典论文的翻译 Glow-TTS:通过单调对齐实现文本到语音的生成流 摘要: 最近,文本到语音(Text-to-Speech,TTS)模型,如FastSpeech和ParaNet ...
- 王道oj/problem7(判断数字是否为对称数)
网址:http://oj.lgwenda.com/problem/7 思路:用temp保存原数: 不断对原数进行/10及取余运算,并加到num2中: 最后判断num2是否与temp相等. 代码: #d ...
- Builder 生成器模式简介与 C# 示例【创建型2】【设计模式来了_2】
〇.简介 1.什么是生成器模式? 一句话解释: 在构造一个复杂的对象(参数多且有可空类型)时,通过一个统一的构造链路,可选择的配置所需属性值,灵活实现可复用的构造过程. 生成器模式的重心,在于分离 ...
- idea 2021新窗口打开工程
描述 Mac M1,IDEA 2017 有部分兼容性问题,遂对IDEA进行升级 idea 升级2021(IntelliJ IDEA 2021.3 (Community Edition)) 打开新工程, ...
- 在 ubuntu 中安装或升级最新版本的 Elasticsearch
1. 安装 java 8 首先最新版本的elasticsearch需要java8的支持,那先来安装一下. $ sudo add-apt-repository -y ppa:webupd8team/ja ...