[WPF]浅析依赖属性(DependencyProperty)

这篇具有很好参考价值的文章主要介绍了[WPF]浅析依赖属性(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。而依赖属性则是省下这些没有用到的属性所需的空间,其关键就在于依赖属性的声明和使用。

依赖属性的声明和使用

依赖属性的使用很简单,只需要以下几个步骤就可以实现:

  1. 让所在类型直接或间接继承自DependecyObject。在WPF中,几乎所有的控件都间接继承自DependecyObject
  2. 声明一个静态只读的DependencyProperty类型变量,这个静态变量所引用的实例并不是通过new操作符创建,而是使用简单的单例模式通过DependencyProperty.Register创建的,下文会对这个方法进行介绍。
  3. 使用依赖属性的实例化包装属性读写依赖属性。
    按照以上步骤可以写出如下代码:
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等委托。

依赖属性存取值的机制

从修饰符可以看出依赖属性是一个静态的只读变量,要确保不同实例的依赖属性正确赋值,肯定不能把数据直接保存到这个静态变量中。这里其实也是依赖属性机制的核心。
与依赖属性存取数据有三个关键的类型:DependencyPropertyDependencyObjectEffectiveValueEntry

  • DependencyProperty:依赖属性实例都是单例,其中DefaultMetadata存储了依赖属性的默认值,提供变化通知、限制、检验等回调以及子类override依赖属性的渠道。GlobalIndex用于检索DependencyProperty的实例。应用程序中注册的所有DependencyProperty的实例都存放于名为PropertyFromName的Hashtable中。
  • DependencyObject:依赖属性的宿主对象,_effectiveValues是一个私有的有序数组,用来存储本对象实例中修改过值得依赖属性,GetValueSetValue方法用于读写依赖属性的数值。
  • EffectiveValueEntry:存储依赖属性真实数值的对象。它可以实现多属性值,具体来说就是内部可以存放多个值,根据当前的状态确定对外暴露哪一个值(这里涉及到多个值选取的优先顺序的问题)。
    [WPF]浅析依赖属性(DependencyProperty)

前边提到依赖属性实例是使用简单的单例模式通过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
FromNameKeyDependencyProperty中的内部私有类,其代码如下:

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中提供了GetValueSetValue方法用于读写依赖属性。先看下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,找到后返回其索引对象EntryIndexEntryIndex主要包含IndexFound两个属性,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中有两个属性:ModifiedValueBaseValueSourceInternalModifiedValue用于跟踪依赖属性的值是否被修改以及被修改的状态。BaseValueSourceInternal是一个枚举,它用于表示依赖属性的值是从哪里获取的。在与ModifiedValue一起使用,可以确定最终呈现的属性值。
EffectiveValueEntryGetFlattenedEntry方法中以下代码及注释可以看出强制值>动画值>表达式值这样得优先级

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,
}

综合起来依赖属性取值优先级列表如下:

  1. 强制:在CoerceValueCallback对依赖属性约束的强制值。
  2. 活动动画或具有Hold行为的动画。
  3. 本地值:通过CLR包装器调用SetValue设置的值,或者XAML中直接对元素本身设置值(包括bindingStaticResourceDynamicResource
  4. TemplatedParent模板的触发器
  5. TemplatedParent模板中设置的值
  6. 隐式样式
  7. 样式触发器
  8. 模板触发器
  9. 样式
  10. 主题样式的触发器
  11. 主题样式
  12. 继承。这里的继承Inherited是xaml树中的父元素,要区别于面向对象语言子类继承(derived,译为派生更合适)与父类
  13. 依赖属性元数据中的默认值

WPF对依赖属性的优先级支持分别使用了ModifiedValueBaseValueSourceInternal,大概是因为约束强制值和动画值是临时性修改,希望在更改结束后能够恢复依赖属性原有值。而对于样式、模板、触发器、主题这些来说相对固定,不需要像动画那样结束后恢复原来的值。

总结

依赖属性是WPF中一个非常核心的概念,涉及的知识点也非常多。像RegisterReadOnlyPropertyMetadataOverrideMetadataAddOwner都能展开很多内容。要想真正掌握依赖属性,这些都是需要熟悉的。文章来源地址https://www.toymoban.com/news/detail-710515.html

到了这里,关于[WPF]浅析依赖属性(DependencyProperty)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [WPF]浅析资源引用(pack URI)

    WPF中我们引用资源时常常提到一个概念: pack URI ,这是WPF标识和引用资源最常见的方式,但不是唯一的方式。本文将介绍WPF中引用资源的几种方式,并回顾一下 pack URI 标识引用在不同位置的资源文件的写法。 WPF中使用URI标识和加载位于各种位置的文件,包括当前程序集资源

    2024年02月05日
    浏览(32)
  • WPF 如何引入图标文件

    我最近在研究WPF,吃饭嘛,桌面端实在是不想用Winform,太丑了。WPF研究好了之后也不会直接去学UWP,MAUI,我是实用主义者,不是技术更新追求者,本身的理念就是能用就行。我现在用的Vue版本还是Vue 2呢。Vue 3?暂时用不上,只是换了个方法去写,写出来的结果还是差不多。

    2024年02月16日
    浏览(32)
  • Maven 引入外部依赖

    如果我们需要引入第三方库文件到项目,该怎么操作呢? pom.xml 的 dependencies 列表列出了我们的项目需要构建的所有外部依赖项。 要添加依赖项,我们一般是先在 src 文件夹下添加 lib 文件夹,然后将你工程需要的 jar 文件复制到 lib 文件夹下。我们使用的是 ldapjdk.jar ,它是为

    2024年02月07日
    浏览(37)
  • WPF使用依赖注入

    现在依赖注入在.Net里面已经普及,自己常写一些简单的demo倒是无所谓,但偶尔写一点正式的工程,也免不了要使用一下,于是总结了一下在WPF里面使用依赖注入。 在写简单Demo时候,通常是在MainWindow的构造函数里面直接做初始化,各种变量也都丢在MainWindow类里面。在使用依

    2024年02月11日
    浏览(25)
  • WPF 用户控件依赖注入赋值

    我一直想组件化得去开发WPF,因为我觉得将复杂问题简单化是最好的 cs部分 我将复杂的依赖注入的代码进行了优化,减少了重复内容的输入。 我现在依赖属性扩展封装在一个静态文件里面 记得UserControl.xaml里面绑定你ViewModel。这样的话有代码提示 我后面要根据Vue的单向数据

    2024年02月07日
    浏览(37)
  • 【FAQ】NPM 引入本地依赖包

    npm 本地依赖包分为 本地文件夹类型 本地文件夹类型的依赖包适用于在编写插件的 dome 示例项目时使用,在无需将包发布到 npm 仓库的情况,做到实时编译,修改 本地压缩包类型 压缩包类型的依赖包适用于没有外网和没有 npm 私有仓库(verdaccio) 的情况下安装依赖 npm 安装文

    2024年01月23日
    浏览(32)
  • springboot中@Test引入的依赖

    一旦依赖了spring-boot-starter-test,下面这些类库将被一同依赖进去: JUnit:java测试事实上的标准,默认依赖版本是4.12  

    2024年02月11日
    浏览(66)
  • 【微信小程序】从 npm 引入第三方依赖时提示依赖异常的解决方法

           微信小程序不允许使用 Node.js 的内建模块。但是部分第三方依赖(如 crypto-js)在运行时会尝试引入 Node.js 内建模块(实际非强制调用),这一举动被微信开发工具误以为是未知的依赖调用。 移除构建 npm 时生成的外部依赖项即可。 1. 找到并打开“/miniprogram_npm/crypto

    2024年02月15日
    浏览(37)
  • WPF实战项目十(API篇):引入工作单元UnitOfWork

    1、通过github地址:https://github.com/arch/UnitOfWork,下载UnitOfWork的代码,将工作单元部分的代码引用到自己的项目,新增UnitOfWork文件夹。 2、在UnitOfWork文件夹下引用UnitOfWork下的IPagedList.cs、PagedList.cs类,WPFProjectAPI项目引用WPFProjectShared项目。 3、然后在program.cs下添加UnitOfWork的服务

    2024年02月16日
    浏览(39)
  • 深入理解WPF中的依赖注入和控制反转

    在WPF开发中, 依赖注入(Dependency Injection)和控制反转(Inversion of Control)是程序解耦的关键,在当今软件工程中占有举足轻重的地位,两者之间有着密不可分的联系 。今天就以一个简单的小例子,简述如何在WPF中实现依赖注入和控制反转,仅供学习分享使用,如有不足之处

    2024年02月06日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包