打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
WPF:依赖属性
userphoto

2020.12.02

关注

WPF:依赖属性

前言:
在使用WPF的时候,总会有一个疑问,依赖属性跟普通的类属性有什么区别,微软要在WPF引入它想要解决什么问题?如果不解除这个疑惑,在编程的时候心里总会不踏实。因此我在网上找了一些资料,终于弄懂了它的由来和机制,特意和大家分享,如有不足,请各位指正!
依赖属性的由来:
在WinForm时代,每个控件类(如TextBox)都会包含许多属性,但是真正用到的少之又少(如text),其他属性就会白白耗费内存资源。那么问题来了,如果只生成一个控件对象,“无用”的属性对性能影响不大,但是当你在一个窗体实例化几十个控件对象的时候,那内存消耗就很可观了。所以,我们应该引入一个机制,在实例化对象的时候,按需“给”它属性,这样子就不会让“无用”的属性白白占用内存资源,因此依赖属性诞生了。
依赖属性的定义:
先比较浅显地讲讲依赖属性的定义,即本身可以没有值,而依赖于其他数据源而取得值的属性。
自定义依赖属性:
在深入了解依赖属性之前,很有必要弄清楚依赖属性是如何敲出来的。假设有一个Person类,现在我要定义一个依赖属性NameProperty,代码如下:

    //依赖属性必须在依赖对象DependencyObject或其子类中定义    class Person : DependencyObject    {        //CLR属性包装器,使得依赖属性NameProperty在外部能够像普通属性那样使用        public string Name        {            get { return (string)GetValue(NameProperty); }            set { SetValue(NameProperty, value); }        }         //依赖属性必须为static readonly,后续讲解        public static readonly DependencyProperty NameProperty =            DependencyProperty.Register("Name", typeof(string), typeof(Person), new PropertyMetadata("DefaultName"));    }代码分析:

  1. 依赖属性必须在依赖对象(DependencyObject)或其子类中定义,因此你能够发现绝大多数WPF控件都是继承自DependencyObject的;
    2.CLR属性Name的作用之一是为了让外界在使用依赖属性时,像使用普通属性一样;
    3.依赖属性约定以Property结尾;
    4.依赖属性必须声明为static readonly,原因后续讲解;
    5.生成依赖属性需要Register方法而不是new,方法的参数依次是CLR属性名、依赖属性对应的类型(注意:所有依赖属性真正的类型是DependencyProperty,这里的意思是依赖属性所“存储”的值的类型)、依赖属性的宿主类、依赖属性的默认元数据;
    6.默认元数据有许多重载,这里使用的是只有一个参数的重载类型,传入的参数是依赖属性的默认值。
    Register方法的内部机制:
    很明显,自定义依赖属性的关键在于Register方法,它的内部机制是:
    1.创建DependencyProperty实例dp;
    2.根据CLR属性名和宿主类型名生成哈希码hashcode,用于唯一标示依赖属性;
    3.在DependencyObject类中有一个全局变量:哈希表PropertyFromName,用于存储所有依赖属性的hashcode,因此在这一步检查是否已经存在相同hashcode,若无则以hashcode和实例dp作为键值对存进哈希表PropertyFromName,否则报错;
    4.最后返回实例dp。
    注意,hashcode并不是实例dp的哈希值,而是一个名为GlobalIndex的int全局变量,由DependencyObject内复杂的算法算出来。
    GetValue和SetValue方法的内部机制:
    也许这个时候你会有疑问:既然依赖属性声明为static,就代表依赖属性只有一份拷贝,那么我定义多个Person类的实例对象时,一旦改变其中一个对象的依赖属性时,其他的对象岂不是都跟着改变?这时候就需要探讨GetValue和SetValue的内部机制了,这里只说GetValue,因为弄懂了GetValue后,对SetValue的机制就自然明白了。
    在GetValue函数中能够看到一个EffectiveValueEntry类的实例,它就好像是一个房间入口(Entry),进去后就能获取想要的值;在每个EffectiveValueEntry的构造函数中有一个名为PropertyIndex的参数,传入的值其实就是上面提到的唯一标示不同实例的GlobalIndex,这样子,每一个实例对象的依赖属性值所对应的入口(Entry)就不一样了;而在DependencyObject类中能够看到 private
     EffectiveValueEntry[] _effectiveValues 这样一个语句,当某个实例的依赖属性被读取时,就会到这个数组检索它对应的EffectiveValueEntry,如果找不到则代表依赖属性还没有被手动赋值(如Name="LiMing"),DependencyObject类的内部算法就会返回依赖属性的默认值(在Register方法第四个参数中设定)。
    这样就明白了,被static关键字所修饰的依赖属性对象真正作用只是用来检索真正的属性值,而不是用来存储值的;而用于检索值的是GlobalIndex,因此为了保证GlobalIndex的稳定性,需要添加readonly关键字。
    所以,依赖属性是以牺牲算法来节省内存空间。
    结语:
    其实依赖属性的内部机制比上述复杂的多,如果想真正理解其中的原理,还是需要各位自己去钻研。Thanks for your attention!

WPF: 只读依赖属性的介绍与实践

在设计与开发 WPF 自定义控件时,我们常常为会控件添加一些依赖属性以便于绑定或动画等。事实上,除了能够添加正常的依赖属性外,我们还可以为控件添加只读依赖属性(以下统称“只读属性”),以增加控件的灵活性。

这听起来有些矛盾。只读依赖属性,只能读不能写,却又怎么能提高控件的灵活性呢?想想我们常用的 IsMouseOver 等属性就可以理解,它们都是只读属性,但如果没有它们,想要控制样式将比较复杂。

所以,总结来说,只读属性的特点是:无法赋值,不能绑定,不能用于动画,不能验证等;而之所以使用它,主要目的是结合属性触发器 (Trigger) 来实现样式的切换

回到顶部

实践

比如,我们要创建一个 FilePicker 控件,用户通过它可以选择文件。那么,它至少包括一个 TextBlock(或 TextBox)和一个 Button,分别用于显示选择文件的路径和打开对话框。

现在我们想实现当用户选择了文件后,控件呈现某种样式。要这么做,我们就可以增加一个 IsFilePicked 只读属性,然后在 ControlTemplate 中添加 Trigger 来控制样式的变化。

1. 创建(定义与注册)

创建只读属性与创建普通依赖属性一样,包括定义、注册、与 CLR 属性包装这三步。不同的是要使用 DependencyProperty 的 RegisterReadOnly 方法来注册,这个方法会返回 DependencyPropertyKey 对象,它包含了对应只读属性的标识符,也就是与它关联的只读属性(通过 DependencyProperty 属性获得),并且对只读属性赋值也是通过它(注意:只读属性自身无法被赋值),代码如下:

// 只读属性的定义与注册private static DependencyPropertyKey IsFilePickedPropertyKey = DependencyProperty.RegisterReadOnly("IsFilePicked", typeof(bool), typeof(FilePicker), new PropertyMetadata(false));public static DependencyProperty IsFilePickedProperty = IsFilePickedPropertyKey.DependencyProperty;

注意其中的命名,因为我们要创建的属性是 IsFilePicked,所以上面两个变量都是在这个名称后加了后辍,分别是 PropertyKey 和 Property,这是命名规范。

另外,我们在元数据的实例中给这个只读属性设置默认值为 false。

2. 包装

然后,将它以 CLR 属性的方式来包装,由于这是个只读属性,所以只需要 get 段就可以,代码如下:

// 只读属性的包装public bool IsFilePicked{get { return (bool)GetValue(IsFilePickedProperty); }}

3. 通过 DependencyPropertyKey 赋值

在合适的位置(当用户选择过文件后),使用 SetValue 方法来赋值,SetValue 有两个重载,要为只读属性赋值,需使用第二个

(DependencyPropertyKey key, object value)

代码如下:

  SetValue(IsFilePickedPropertyKey, true);

4. 应用

逻辑写好后,在模板中增加以下 XAML 代码,即可:

<ControlTemplate.Triggers><Trigger Property="IsFilePicked" Value="True"><!--显示绿色边框--><Setter Property="BorderBrush" Value="Green" /><Setter Property="BorderThickness" Value="2" /></Trigger></ControlTemplate.Triggers>

总结

本文简单介绍了在 WPF 中如何创建以及使用只读依赖属性,合适地使用它,能够使我们更灵活地实现对自定义控件样式的控制。


WPF --依赖属性详解

依赖项属性可以称得上是WPF中比较难理解的概念,为了搞清楚这个概念,我都把.NET类库进行了反编译,但是,其结果我也是想到了的,微软不是傻子,.NET那么庞大,就算能被你反编译了,你也看不懂它的代码。

所以说,经过我一番研究,虽然没有把.NET的每一行代码都弄明白,不过,黄天终不负有心人,依赖项属性的使用方法与基本原理,我可以说已经弄明白了,恰巧,前两天在网上看到一篇讨论依赖项属性的文章,写得还不错,作者估计也是一位高人,再加上我个人的研究,从实际应用的角度来说,我现在已经掌握了依赖项属性的使用方法了,不妨告诉你,其实很简单,可以这么说,整个WPF都很简单,和许多刚接触WPF的朋友一样,一开始我也是认为它很复杂很难懂。

为什么这样说呢?大家都知道,微软官方总是为它自己推出的产品配备很完备的文档,对,就是那个很出名的MSDN。

许多初学WPF的朋友,一定也会像我一样,去查阅MSDN,通过上面的介绍来入门,可杯具正是发生在这个时候,WPF的难懂难学就是被微软自己的文档所误导,先别说翻译的质量不好,就算你看英文原文,你大概也会看得头晕。

真的,那些概念模型实在太抽象了,从刚接触WPF到现在,我都不知道把MSDN翻了多少遍了,甚至查到微软都把我的IP列入黑名单了,呵呵,而且,  我也下载了英文原版的SDK来对比研究。

说实话,对.NET类库进行反编译的学习方法效率很低,表面上说可以更深入地了解.NET框架,但是,我不推荐这样学习,真的,得不尝失,花费很多精力和时间,而收获甚少;还有就是,反编译.NET类库是属于侵权,哈,幸好我们都生活在没有法律的中国,不然,一定会被微软告上法庭。

这次反编译,完全出于无奈,因为有些概念的确难以理解。

在研究的同时,我也进行了反思,最后感悟是——还是那句老话:理论的东西,哪怕你把它背下来了,你永远也不懂。

对我们来说,学编程为了什么?不就是为了应用吗?也就是说用于实战,既然这样,其实我们不必把理论的东西钻得太死,不然,钻牛角尖容易走火入魔。

依赖项属性的重点在于“依赖”二字,既然是依赖了,也就是说:依赖项属性的值的改变过程一定与其它对相关,不A依赖B就B依赖A,或者相互依赖。

说白了,所谓依赖,主要应用在以下地方:

1、双向绑定。有了这个,依赖项属性不用写额的代码,也不用实现什么接口,它本身就俱备双向绑定的特性,比如,我把员工对象的姓名绑定到摇文本框,一旦绑定,只要文本框中的值发生改变,依赖项属性员工姓名也会跟着变化,反之亦然;

2、触发器。这个东西在WPF中很重要,比如,一个按钮背景是红色,我想让它在鼠标停留在它上面是背景变成绿色,而鼠标一旦移开,按钮恢复红色。

如果在传统的Windows编程中,你一定会想办法弄一些事件,或者委托来处理,还要写一堆代码。告诉你,有了依赖项属性,你将一行代码都不用写,所有的处理均由WPF属性系统自动处理。而触发器只是临时改变属性的值,当触完成时,属性值自动被“还原”。

3、附加属性。附加属性也是依赖项属性,它可以把A类型的的某些属性推迟到运行时根据B类型的具体情况来进行设置,而且可以同时被多个类型对象同时维护同一个属性值,但每个实例的属性值是独立的。

4、A属性改变时,也同时改变其它属性的值,如TogleButton按下的同时,弹出下拉框。

为了进行比较,我们先来说说传统面向对象编程中对类属性的定义,请看下面一个简单的类,它只有一个公共属性。

public class Student{public string Name { set; get; }}

这时候,我们布局一下WPF主窗口,如下所示XAML:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="180" Width="300" Loaded="Window_Loaded">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Column="0" Grid.Row="0" Text="姓名:"/>
        <TextBox x:Name="txtName" Grid.Column="1" Grid.Row="0" Margin="0,5,20,5"/>
        <TextBlock Grid.Column="0" Grid.Row="1" Text="改变值:"/>
        <TextBox x:Name="txtCh" Grid.Column="1" Grid.Row="1" Margin="0,5,20,5"/>
        <Button x:Name="btn" Grid.Row="2" Grid.ColumnSpan="2" Margin="70,5,70,5" Click="btn_Click">显示属性值</Button>
    </Grid>
</Window>

在窗口的加载完成事件中,我们作两个绑定:

(1)把Student的实例的Name属性与textBox的text属性绑定;

(2)同时与第二个文本框也绑定。

运行程序,在第一个文本框中输入内容,再点一下第二个文本框,或点一下按钮,虽然第二个文本框也会随之改变,但并不是同步改变,而是当焦点离开第一个文本框后才发生改变,这就不属于同步了。

完整代码如下:

Student myStu = new Student { Name = "小明" };private void Window_Loaded(object sender, RoutedEventArgs e){//绑定数据BindingOperations.SetBinding(txtName, TextBox.TextProperty,new Binding { Source = myStu, Path = new PropertyPath("Name"), Mode = BindingMode.TwoWay });BindingOperations.SetBinding(txtCh, TextBox.TextProperty,new Binding { Source = myStu, Path = new PropertyPath("Name"), Mode = BindingMode.TwoWay });}private void btn_Click(object sender, RoutedEventArgs e){MessageBox.Show("当前实例的属性值:Name = " + myStu.Name);}

在上一文中,我们用传统面向对象的方法来定义了一个类,而我们同时把该类的实例绑定到两个文本框,第一个文本框用于输入值,第二个文本框用于根据第一个文本框中的输入来取得属性值。

在上例中我们已经明了,虽然能做到同步更新,但这同步更新并不是实时的。而是在控件失去焦点或点击按钮之后才发生,因为那个时候是重新进行了绑定,所以,一般的属性声明并没有实现实时更新。

下面,我们把Student类进行改动,把Name属性改为依赖项属性。

public class Student:DependencyObject{//注册依赖项属性public static readonly DependencyProperty NameProperty =DependencyProperty.Register("Name",typeof(string),typeof(Student),new PropertyMetadata(string.Empty));}

从定义中我们看到依赖项属性的定义规则:

1、必须是公开的静态字段,public static;

2、因为是静态成员而且是公开的,有可能被恶意或无意修改,为了保险,加上一个readonly关键字;

3、调用静态方法Register返回一个DependencyProperty实例。

从上述内容中,可以进一步分析,依赖项属性是通过注册到WPF属性系统来定义的, Register方法有多种重载,示例中用到的是以下签名:

  public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata);
 

       //
        // 摘要:
        //     使用指定的属性名称、属性类型、所有者类型和属性元数据注册依赖项属性。
        //
        // 参数:
        //   name:
        //     要注册的依赖项对象的名称。
        //
        //   propertyType:
        //     属性的类型。
        //
        //   ownerType:
        //     正注册依赖项对象的所有者类型。
        //
        //   typeMetadata:
        //     依赖项对象的属性元数据。
        //
        // 返回结果:
        //     一个依赖项对象标识符,应使用它在您的类中设置 public static readonly 字段的值。然后,在以后使用该标识符引用依赖项对象,用于某些操作,例如以编程方式设置其值,或者获取元数据。

这里说一下ownerType参数,它只的是注册依赖项属性的类型,如本例中就是Student类。

typeMetadata是所谓的元数据,就是属性的默认,当然,它也有N个构造函数,可以同时传递事件委托来对属性的改性或类型转换时进行事件处理。

下面一点很重要,就是属性的命名格式,根据约定,必须为以下格式:

XXXProperty,你的属性名后面紧跟Property,记住!

我们把前面的示例改一下,作以下绑定

//声明一个Student类Student myStu = new Student();private void Window_Loaded(object sender, RoutedEventArgs e){myStu.SetValue(Student.NameProperty, "小张");//绑定到第一个文本框BindingOperations.SetBinding(myStu, Student.NameProperty,new Binding{Source = txtName,Path = new PropertyPath("Text")});//绑定第二个文本框BindingOperations.SetBinding(txtChanged, TextBox.TextProperty,new Binding{Source = myStu,Path = new PropertyPath(Student.NameProperty)});}

这时候你运行程序,当你在第一个文本框中输入内容时,第二个的文本框就会立即发生改变,这就说明,在双向绑定的模式下,属性的更新是实时的。

好,为了能像普通CLR属性一样使用,我们对Student类做一些封装。在封装之前,说一下是如何获取和设置属性的。

在WPF中,要使用依赖项属性,该类必须继承DependencyObject类,DependencyObject类有两个属性专门处理属性的值。

(1)GetValue用于获取值;

(2)SetValue用于设置值。

于是,我们对Student类作以下封装:

public class Student:DependencyObject{//注册依赖项属性public static readonly DependencyProperty NameProperty =DependencyProperty.Register("Name",typeof(string),typeof(Student),new PropertyMetadata(string.Empty));public string Name{get{return (string)this.GetValue(NameProperty);}set{this.SetValue(NameProperty, value);}}}

虽然现在已经知道如何使用依赖项属性了,但是,依赖项属性是如何注册到WPF的属性系统中的,我们似乎一头雾水。

要把这一问题搞清楚,只有一种办法,那就是把.NET类库反编译,工具不用我说了,网上大把,呵,这可是侵权的,靠,没办法,谁叫微软不开源。

通过反编译得到Register方法的定义如下,其实每个重载都是调用了下面这个方法:

public static DependencyProperty Register(string name, Type propertyType, Type ownerType, PropertyMetadata typeMetadata, ValidateValueCallback validateValueCallback){    RegisterParameterValidation(name, propertyType, ownerType);    PropertyMetadata defaultMetadata = null;    if ((typeMetadata != null) && typeMetadata.DefaultValueWasSet())    {        defaultMetadata = new PropertyMetadata(typeMetadata.DefaultValue);    }    DependencyProperty property = RegisterCommon(name, propertyType, ownerType, defaultMetadata, validateValueCallback);    if (typeMetadata != null)    {        property.OverrideMetadata(ownerType, typeMetadata);    }    return property;}

其它的不用看,我们只找重点语句,上面代码片段中,加粗斜体部分为重点,也就是说,所以的依赖项属性的注册,都是调用了RegisterCommon方法的。

接着,我们来看看RegisterCommon方法是如何定义的。

这段代码有点长,我就不全部复制过来了,我把重要的语句拿过来。

private static DependencyProperty RegisterCommon(string name, Type propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback validateValueCallback){    FromNameKey key = new FromNameKey(name, ownerType);    lock (Synchronized)    {        if (PropertyFromName.Contains(key))        {            throw new ArgumentException(SR.Get("PropertyAlreadyRegistered", new object[] { name, ownerType.Name }));        }    }
       //省略代码.................
    return dp;}

大家注意加粗斜体字标注的地方,其中有一个if语句判断,是否在PropertyFromName中存在某个key,如果存在就抛出异常,那么这个PropertyFromName究竟是什么呢? 

好,带着疑问,继续跟踪代码,在DependencyProperty类中发现了它的定义:

private static Hashtable PropertyFromName;

这下就明白了,原来刚来所检查的key的容器就是一个哈希表,而且是全局的。

上面的代码告诉我们,哈希表的key就是一个FromNameKey ,

    FromNameKey key = new FromNameKey(name, ownerType);

那么,这个FromNameKey又是如何计算的呢?继续反编译,我找到了它的定义:

private class FromNameKey{// Fields    private int _hashCode;private string _name;private Type _ownerType;// Methodspublic FromNameKey(string name, Type ownerType){this._name = name;this._ownerType = ownerType;this._hashCode =this._name.GetHashCode() ^this._ownerType.GetHashCode();}    //省略代码...............}希哈值的计算方法是:(类名 + 属性名).HashCode();这样一来,就可以保证依赖项属性只能注册一次,因为哈希值是全局唯一的。而且,我们知道DependencyProperty类有个GlobalIndex,这个索引就是依赖项属性在哈希表中的位置,当我们要获取属性的值或要设属性的值,就从这个索引表里寻找。

上节中,我们分析了依赖项属性的注册和定义方法,并解释了依赖项属性的注册过程,但是,有一个疑问会困惑着我们,既然依赖项属被声明为静态只读字段,那为什么它的值可以被改变呢?难道你不觉得很奇怪吗?

微软的葫芦里到底卖的什么药呢?我们来看看。

前文中我们提到过,设置依赖项属性的值使用SetValue方法,那好,我们就从SetValue方法入手。

SetValue方法的定义如下:

public void SetValue(DependencyProperty dp, object value){base.VerifyAccess();PropertyMetadata metadata = this.SetupPropertyChange(dp);this.SetValueCommon(dp, value, metadata, false, OperationType.Unknown, false);}

VerifyAccess方法是用于检查可访问性,这个不管它,重点是看SetValueCommon方法,它才是真正设置值的方法,好,继续跟入,看看SetValueCommon方法的定义:

private void SetValueCommon(DependencyProperty dp, object value, PropertyMetadata metadata, bool coerceWithDeferredReference, OperationType operationType, bool isInternal){if (this.IsSealed){throw new InvalidOperationException(SR.Get("SetOnReadOnlyObjectNotAllowed", new object[] { this }));}Expression expr = null;DependencySource[] newSources = null;EntryIndex entryIndex = this.LookupEntry(dp.GlobalIndex);if (value == DependencyProperty.UnsetValue){this.ClearValueCommon(entryIndex, dp, metadata);}else{EffectiveValueEntry entry;EffectiveValueEntry entry2;bool flag = false;bool flag2 = value == ExpressionInAlternativeStore;if (!flag2){bool flag3 = isInternal ? dp.IsValidValueInternal(value) : dp.IsValidValue(value);if (!flag3 || dp.IsObjectType){expr = value as Expression;if (expr != null){if (!expr.Attachable){throw new ArgumentException(SR.Get("SharingNonSharableExpression"));}newSources = expr.GetSources();ValidateSources(this, newSources, expr);}else{flag = value is DeferredReference;if (!flag && !flag3){throw new ArgumentException(SR.Get("InvalidPropertyValue", new object[] { value, dp.Name }));}}}}if (operationType == OperationType.ChangeMutableDefaultValue){entry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Default) {Value = value};}else{entry = this.GetValueEntry(entryIndex, dp, metadata, RequestFlags.RawEntry);}object localValue = entry.LocalValue;Expression expression2 = null;Expression expression3 = null;if (entry.HasExpressionMarker){if (expr == null){expression3 = _getExpressionCore(this, dp, metadata);}if (expression3 != null){localValue = expression3;expression2 = expression3;}else{localValue = DependencyProperty.UnsetValue;}}else{expression2 = entry.IsExpression ? (localValue as Expression) : null;}bool flag5 = false;if ((expression2 != null) && (expr == null)){if (flag){value = ((DeferredReference) value).GetValue(BaseValueSourceInternal.Local);flag = false;}flag5 = expression2.SetValue(this, dp, value);entryIndex = this.CheckEntryIndex(entryIndex, dp.GlobalIndex);}if (flag5){if (entryIndex.Found){entry2 = this._effectiveValues[entryIndex.Index];}else{entry2 = EffectiveValueEntry.CreateDefaultValueEntry(dp, metadata.GetDefaultValue(this, dp));}}else{entry2 = new EffectiveValueEntry(dp, BaseValueSourceInternal.Local);if ((expression2 != null) && (expression2 != expression3)){DependencySource[] sources = expression2.GetSources();UpdateSourceDependentLists(this, dp, sources, expression2, false);expression2.OnDetach(this, dp);entryIndex = this.CheckEntryIndex(entryIndex, dp.GlobalIndex);}if (expr == null){entry2.IsDeferredReference = flag;entry2.Value = value;entry2.HasExpressionMarker = flag2;}else{this.SetEffectiveValue(entryIndex, dp, dp.GlobalIndex, metadata, expr, BaseValueSourceInternal.Local);object defaultValue = metadata.GetDefaultValue(this, dp);entryIndex = this.CheckEntryIndex(entryIndex, dp.GlobalIndex);this.SetExpressionValue(entryIndex, defaultValue, expr);UpdateSourceDependentLists(this, dp, newSources, expr, true);expr.MarkAttached();expr.OnAttach(this, dp);entryIndex = this.CheckEntryIndex(entryIndex, dp.GlobalIndex);entry2 = this.EvaluateExpression(entryIndex, dp, expr, metadata, entry, this._effectiveValues[entryIndex.Index]);entryIndex = this.CheckEntryIndex(entryIndex, dp.GlobalIndex);}}this.UpdateEffectiveValue(entryIndex, dp, metadata, entry, ref entry2, coerceWithDeferredReference, operationType);}}

第一个引起我注意的是DependencySource类,它封装了依赖性对象和依赖性属性,并分别为两个只读属性。

还有一个比较核心的类就是EffectiveValueEntry.

internal EffectiveValueEntry(DependencyProperty dp, BaseValueSourceInternal valueSource){this._propertyIndex = (short) dp.GlobalIndex;this._value = DependencyProperty.UnsetValue;this._source = (FullValueSource) valueSource;}

FullValueSource是一个枚举,如果我没猜错的话,它就是用来标识属性的来源,它有以下几个值:

[FriendAccessAllowed]internal enum FullValueSource : short{HasExpressionMarker = 0x100,IsAnimated = 0x20,IsCoerced = 0x40,IsDeferredReference = 0x80,IsExpression = 0x10,ModifiersMask = 0x70,ValueSourceMask = 15}

1、通过XAML标记语言来改变属性值,如:

<TextBox x:Name="txt" Text="这是一个测试" />

这里就通过XAML标记为 Text属性赋了值。

2、受动画影响而改变的值。比如我画一个矩形,在动画面板中我让它演示长达6秒钟的动画,这期间,矩形的X坐标从20变为80,这个值就是通过动画来设置属性值,这个值是不会提交到属性更改,只是临时更改,动画停止后,属性值将会恢复为原来的值。

3、强制设置值。这个概念很奇怪,可以把它理解为对属性的输入值进行类型转换,我们知道,WPF里面提供了许多XXXXConvertor,毕竟我们在XAML标记中只能输入文本值,比如颜色,我们都会输入Red这样的值,其实在后台,运行库把字符串的值转换为画刷实例,再向属性赋值。

4、被改变后的值,从跟踪的结果看,依赖项属性的值都有N个版本,分别为不同的变量来保存,以便做验证和对比。

EffectiveValueEntry里面就是保存依赖项属性的全局索引和属性值,继续反编译,发现了一个InsertEntry方法,比较长,代码我不帖,只帖签名。

private void InsertEntry(EffectiveValueEntry entry, uint entryIndex)

两个参数都很好理解,第二个参数就是数组的索引,那么,是哪个数组的索引呢?接着反编译,看到DendencyObject类有一个内部字段,定义如下:

private EffectiveValueEntry[] _effectiveValues;

对,这下找到了,就是一个EffectiveValueEntry的数组,好了,不用往下跟了,到这里基本上可以看出依赖项属性是如何保存它的值了,原理如下:

每个DendencyObject类的实例都会创建一个专门的数组,数组中的每个元素分别标识着该类型的一个依赖项属性,例如我定义了一个类A,A里面定义了3个依赖项属性A.kk,A.cc,A.ff,这样当A类被分配到内存中实例化的时候,创建一个EffectvieValueEntry数组,而每个EffectiveValueEntry对应着一个属性,保存着该属性的不同版本的值,A类型有3个依赖项属性,所以对应的EffectiveEntry数组就有3个元素。

那么,CLR如何知道哪个EffectiveEntry对应着哪个依赖项属性呢?前面说过,每个依赖项属性都会以哈希值的键保存到一个全局哈希表中,所以,只要查找出对应键就可以唯一地标识依赖项属性了。

既然知道了如何设置值,那么,对于如何获取值就更好办了,过程刚好相反。

public object GetValue(DependencyProperty dp){base.VerifyAccess();if (dp == null){throw new ArgumentNullException("dp");}return this.GetValueEntry(this.LookupEntry(dp.GlobalIndex), dp, null, RequestFlags.FullyResolved).Value;}

看到最后一行,其实返回了一个EffectiveValueEntry类型的变量,再从中取出Value属性的值,这个值其实就是对应类型的依赖项属性的值。

那么,EffectiveValueEntry数组中的元素是在哪儿赋值的呢?我翻遍了整个DependencyObject类也没有找到赋值的语句,这时候我突然想起刚才的SetValue方法,注意到这几行:

if (operationType == OperationType.ChangeMutableDefaultValue){entry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Default) {Value = value};}else{entry = this.GetValueEntry(entryIndex, dp, metadata, RequestFlags.RawEntry);}

如果要设置的值的等于默认,还记得我们调用Register注册依赖项属性时,最后一个参数,它是一个PropertyMetaData类型,它封装了依赖项属性的默认值,也就是元数据,元数据说白了就初始值,“元”在汉语中有“首”,“开始”,“第一”等意思,如“XX元年”、“公元2002年”等。

所以,上面的代码是:如果设置的值与元数据一致,就不用赋值了,直接从元数据里面,这样可以免去了在赋值过程发生的“拆箱”和“装箱”行为消耗性能;

如果设置的值不是元数据(默认值),那么就动态创建一个EffectvieValueEntry类的实例,并保存当前值。

因此,EffectiveValueEntry数组是动态分配内存的,就是说,如果你不对属性进行赋值,那就不创建EffectiveValueEntry实例,因为依赖项属性在静态注册时就带了默认值。

因此,对比传统的面向对象属性系统,WPF的依赖项属性有以下优点:

1、类被实例化时不对属性进行初始化,大大节省了内存空间。

在传统的属性系统中,比如一个类B有1000个属性,每个属性都是int类型,而整型每个实例占用4个字节的内存,初台化1000个属性,就等于分配1000个整型的有效内存,也就是说,B类实例化后将占用 1000 * 4 = 4000字节的内存,如果我的类有100000个字符串的属性,而这些属性都是保存中文字符,你可想象有多么恐怖!

WPF的依赖项属性系统对每个类初始化时并不初始化属性,而是等到运行时需要设置才进行分配,这样一来,就大大节省了内存占用率。

2、更高的灵活性。由于依赖项属性的声明是静态的,也就是说,它存在于整个应用程序生命命周期内,它并不属于特定类,如Content属性,它可以属于Canvas类,也可以属于DockPanel类,只要在全局哈希表中键值不重复即可。

还有就是附加属性,附加属性其实也是依赖项属性,比如一个会计的职责是处理帐目和财务相关的事情,有时候质量控制部门比较忙,财务部门的职责就多了一个检测的任务,但这个职责并不是财务人员所必须的,只是在需要时才存在。

附加属性就是这样,可以把其它类的属性附加到当前类的属性列表,而这些属性并不是必须的,需要时就存在,不需要时就不存在,如DockPanel类的Dock就是附加在其子元素中,因为DockPanel可能放N个元素,你无法用一个值来固定它们的位置,可能一些元素Top,或者有些元素Left,这样一来,只能把Dock属性附加在它们身上,按具体需要来设置。

3、继承性。XAML中元素的属性值可以从其容器标记或父节点继承而来的属值。如路由事件的泡冒行为就是一个典型。DockPanel里面放置了4个按钮,我只需在DockPanel的XAML声明中加入Button.Click="Button_Click",这样,子元节点在的所有按钮都会捕捉该事件,并调用同一个事件处理过程,这在传统的WinForm中是做不到的。

4、本地值。动画和3D变换等会改变对象的属性,但是,这些改变是临时,动画停止或删除后,该改变就不存在了。

5、数据绑定。依赖项自身具备实时更新功能,不需要我们手动去编写代码实现;WPF的数据绑定不仅仅在数据源,资源、样式、其它元素的属性值都可以实时更新。

6、资源。包括动态资源和静态资源的引用。这个我不多说了,请自行参考MSDN,上面都详细介绍了。

……

但是,依赖项属性并不是没有缺点的,它的缺点正是来自于它的优点。

由于依赖项属性是静态只读字段,所以应用程序启动时就必须创建所有的依赖项属性(注意,是创建用DependencyProperty.Register方法注册的DependencyProperty,不包括属性值),这就需要时间了,这就是为什么WPF程序启动速度慢的原因,它是用“时间”来换取“空间”,据说,微软一直在改善这问题,到了.NET3.5 SP1的时候,就有了明显的改进。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
一站式WPF
闲话WPF之九(Dependency属性 [1] )
WPF学习笔记三 依赖属性
【WPF学习】第十二章 属性验证
WPF中PasswordBox的简单用法 | 学步园
WPF基础到企业应用系列7——深入剖析依赖属性(WPF/Silverlight核心)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服