WPF - 属性系统 (1 of 4)

  本来我希望这一系列文章能够深入讲解WPF属性系统的实现以及XAML编译器是如何使用这些依赖项属性的,并在最后分析WPF属性系统的实际实现代码。但是在编写的过程中发现对WPF属性系统代码的讲解要求之前的介绍能触及到属性系统的方方面面。而且其内部实现代码涉及到了众多的内部算法,对它们进行讲解反而可能导致读者产生更多迷惑。因此我最终改变了初衷,将这一系列文章重新定义为介绍WPF属性系统所提供的各种功能,并伴随各个功能讲解WPF属性系统的实际实现方式。

  本系列文章将从最基础的有关依赖项属性的知识讲起,并最终深入到WPF属性系统的内部实现。通过这一系列文章,希望您能了解有关WPF属性系统的各个方面,并对您的日常编程有所裨益。

 

依赖项属性的组成

  WPF属性系统所提供的各个功能主要是通过依赖项属性来暴露的。因此了解属性系统的最重要方式就是了解一个依赖项属性到底提供了什么样的功能。在本节中,我们将对这篇文章所提到的依赖项属性功能进行一次简单的介绍。

  首先是依赖项属性的组成。如果需要为一个类型定义一个依赖项属性,那么该类型首先需要从DependencyObject类派生,以获得对属性系统的支持。而在依赖项属性的标准实现中,一个依赖项属性会在该类型上暴露一个DependencyProperty类型的公有静态成员,以作为该依赖项属性的ID。例如ContentControl类的ContentProperty静态属性。同时,软件开发人员还需要暴露依赖项属性的CLR包装,从而允许用户在编写代码的过程中直接通过Content属性设置该依赖项属性的值:

1 ContentControl control = new ContentControl();
2 control.Content = new Button();

  可以看到,用户代码对依赖项属性的使用与普通的.net属性并没有什么区别。这一切都需要归功于CLR属性对依赖项属性的包装。该包装内则包含实际的对WPF属性系统进行操作的代码。

  当然,依赖项属性比普通的CLR属性提供了更为丰富的功能,否则WPF不会大动干戈地重新实现一个属性系统。可以说,WPF中各个功能的实现都离不开依赖项属性的支持,如绑定,模板,动画,属性值的优先级,属性值的继承以及属性值的矫正和更改回调等。实现对这些功能的支持则非常简单:在创建一个依赖项属性的时候,软件开发人员需要通过元数据来标明对各个功能的支持,并可以传入自定义的函数作为属性更改的回调。同时,一个派生类还可以重新设定基类注册依赖项属性时所提供的设置,也支持一个类型通过AddOwner()函数调用将另一个类型中所定义的依赖项属性添加为自己的依赖项属性。

  但使用依赖项属性的最重要原因就是依赖项属性拥有更好的性能。您可能怀疑,如果用普通的.net属性实现方式实现一个属性已经非常简单了:

private Object _content;

public Object Content
{
    get { return _content; }
    set { _content = value; }
}

  在每个访问符中,对属性的取得和设置仅仅是一行语句,哪里还有提升空间呢?实际上,依赖项属性所指的性能提高并非是指的时间,而是指依赖项属性对于空间的节省。在普通的CLR属性中,软件开发人员需要为每个属性提供一个相应的私有成员变量,以记录各属性的值。但是实际情况呢?在一个控件所提供的几十个属性中,我们常常使用其中的几个就完成了对该控件的设置。这再加上WPF对模板以及内容模型的支持,因此使用普通CLR属性编写一个复杂UI所占用的内存是巨大的。

  而WPF属性系统的实现则使用了Flyweight模式:在每个DependencyObject类型的函数中都拥有一个名为_effectiveValues的成员。该成员是一个EffectiveValueEntry类型的数组。其按照依赖项属性ID的升序记录了所有已赋值的依赖项属性的值。在尝试获取一个依赖项属性的值时,WPF属性系统将首先从该数组中查找该依赖项属性所对应的记录。如果该记录存在,那么该记录所包含的依赖项属性值将返回,否则WPF属性系统将返回该依赖项属性的默认值。其运行过程如下:(.net源码摘录,已展开)

object GetValue(DependencyProperty dp)
{
    // 从_effectiveValues中查找当前依赖项属性所对应的条目
    EntryIndex entryIndex = this.LookupEntry(dp.GlobalIndex)
    EffectiveValueEntry entry;
    if (entryIndex.Found)
    {
        entry = this._effectiveValues[entryIndex.Index];
    }
    else
    {
        // 创建默认的条目,其值为UnsetValue
            entry = new EffectiveValueEntry(dp, BaseValueSourceInternal.Unknown);
    }

    if (entry.Value != DependencyProperty.UnsetValue)
    {
        return entry.Value; // 返回条目中所记录的有效值
    }

    // 如果条目中记录的值是无效的,那么创建该依赖项属性的默认条目,其所包含的值为默认值
    PropertyMetadata metadata = dp.GetMetadata(this.DependencyObjectType);
    return EffectiveValueEntry.CreateDefaultValueEntry(dp, metadata.GetDefaultValue(this, dp)).Value;
}

  所以说,WPF属性系统所指的更有效率并非是在执行时的属性访问速度,而是在一定程度上牺牲了执行速度而带来的内存占用降低。使用Flyweight模式实际上是大型应用程序实现中的一个常见选择。在程序中包含大量的具有相同类型的特定类型数据时,我们可以通过该模式非常有效地降低其所占用的内存。

 

定义依赖项属性

  在讲解依赖项属性是如何支持各功能之前,让我们首先来讲解一下依赖项属性的创建到底向WPF属性系统输入了哪些信息。

  通过DependencyProperty.register()创建一个依赖项属性的代码如下:

public static readonly DependencyProperty HintProperty = 
    DependencyProperty.Register("Hint", typeof(String), typeof(AutoCompleteEdit),
        new FrameworkPropertyMetadata(String.Empty),
        new ValidateValueCallback(IsHintValid));

  这里,我们使用了该函数的一个最复杂的重载。该重载包含了创建依赖项属性时所可以设置的所有参数。首先,软件开发人员需要在Register()函数中标明需要创建的依赖项属性的名称。在上面的例子中,该依赖项属性将拥有一个名称Hint。该属性将被注册在类型AutoCompleteEdit上,并且其自身类型为String。在没有为属性赋值的情况下,该属性将拥有默认值String.Empty,并在数值将要发生改变时调用验证函数IsHintValid()。

  在通过Register()函数注册一个DependencyProperty的时候,我们需要将接受该返回值的静态变量设置为readonly。这是因为使用该关键字修饰的属性一旦被赋值就不可以被更改,进而允许编译器对其进行优化。

  而在WPF属性系统内部,DependencyProperty.Register()函数到底做了什么事情呢?        在.net源码中搜索该函数可以发现,无论是用来注册依赖项属性的Register()函数还是用来注册附加属性的RegisterAttached()函数,实际上其内部都会调用一个通用的函数RegisterCommon()。那是不是说依赖项属性和附加属性实际上只是WPF属性系统所支持的同一个组成的不同暴露方式呢?实际上的确如此。对于属性系统而言,一个附加属性实际上就是一个依赖项属性,只是鉴于依赖项属性和附加属性在使用方式上的不同,WPF属性系统按照不同的方式暴露了不同的API。因此,WPF依赖项属性所支持的各种功能也被附加属性所支持。

  让我们继续向下看。在RegisterCommon()函数中,我们可以看到如下代码:

private static DependencyProperty RegisterCommon(string name, Type propertyType,
    Type ownerType, ……)
{
    // 创建一个有关依赖项属性名称以及所在类型的结构,并将作为依赖项属性查找的Key
    FromNameKey key = new FromNameKey(name, ownerType);
    ……
    DependencyProperty dp = new DependencyProperty(name, propertyType, ownerType, defaultMetadata, validateValueCallback);
    defaultMetadata.Seal(dp, null);
    ……
    lock (Synchronized)
    {
        // 插入到静态成员DependencyProperty.PropertyFromName中
        PropertyFromName[key] = dp;
    }
    ……
    return dp;
}

  从上面的代码摘要中可以看到,RegisterCommon()函数仅仅创建了一个DependencyProperty的实例,并将其以属性名以及其所在类型作为关键字记录在私有静态成员PropertyFromName中。也就是说,当前程序的所有依赖项属性都将记录在该私有成员中。

  很多属性系统所暴露的API都使用了成员PropertyFromName所记录的内容,如DependencyProperty.FromName()函数就是基于它实现的。同时,这种集中性的注册也说明了一个问题:在属性系统中,依赖项属性实际上并不是存储在特定类型中,而是以类型作为Key的一个组成部分存储在一个全局变量中。这也就解释了WPF属性系统在使用过程中所出现的与普通CLR属性所不太一致的地方。如为什么我们通过对依赖项属性值的读写需要通过Register()函数所返回的DependencyProperty实例,而不是直接使用属性名称。

  在定义一个依赖项属性的时候,DependencyProperty.Register()函数所传入的表示依赖项属性的名称常常是导致依赖项属性工作不正常的一个重要原因。较为常见的一个错误就是,依赖项属性注册时所使用的名称与依赖项属性的名称并不对应。在Style、Template以及普通的依赖项属性设置过程中,WPF内部都是通过“属性名”+“Property”的方式在属性系统内寻找对应的依赖项属性的。例如在XAML中为一个FrameworkElement元素的Name属性赋值时,XAML编译器将会把该赋值转化为对ID为NameProperty的依赖项属性赋值。如果在调用DependencyProperty.Register()函数时传入的表示依赖项属性名称的参数与类型中记录的依赖项属性ID并不匹配,那么对这些依赖项属性的使用就会失效。

  另外一个事情就是,派生类会自动继承基类的依赖项属性。您可能心里有些疑问:派生类和基类应该是两个不同的类型,因此由派生类和属性名所共同组成的Key应该无法访问基类所定义的依赖项属性。但实际的情况就是,对一个依赖项属性的获取将从当前类型开始沿继承层次向上,直到达到类型继承层次的顶端,或通过类型以及属性名的组合找到相应的依赖项属性为止。通过这种方法,基类中所定义的各个依赖项属性对派生类可见。

  同时,WPF所使用的这种依赖项属性的寻找方式使依赖项属性的重写变为了可能。在一个DependencyObject类的派生类中,软件开发人员可以通过DependencyProperty.Register()函数注册一个具有相同名称的依赖项属性,以完成对依赖项属性的重写。从此在该类型以及其派生类中查找该属性时,WPF属性系统会返回这个新注册的依赖项属性。通过DependencyProperty.Register()函数重写属性的最大好处在于,它可以更改依赖项属性的类型。这是OverrdieMetadata()以及AddOwner()等函数所做不到的。

  在将一个依赖项属性添加到属性系统之后,我们需要为该依赖项属性添加一个CLR属性包装:

public String Hint
{
    get { return (String)GetValue(HintProperty); }
    set { SetValue(HintProperty, value); }
}

  在该CLR属性包装中,我们需要通过GetValue()以及SetValue()函数得到或设置依赖项属性的值。由于WPF内部对某些依赖项属性的使用是直接通过调用GetValue()以及SetValue()来完成的,因此软件开发人员最好在CLR属性包装直接调用GetValue()以及SetValue()函数,而不添加其它的自定义逻辑,否则在使用某些功能时,如执行在XAML中声明的触发器时,看起来对某个属性的设置却最终仅仅调用了GetValue()及SetValue()函数。

  需要注意的是,我们需要在程序中谨慎使用SetValue()函数。就和HintProperty的声明一样,一个依赖项属性的ID将会以静态公有成员存在于一个类型中。在该类型之外的程序代码可以通过SetValue()函数以及该ID对依赖项属性进行赋值。这样问题就来了:首先,SetValue()属性可以接受任何类型的实例,因此用户代码极容易出现为属性所设置的类型不对的情况。另外一点则是,对该函数的调用将会清空当前程序所声明的对该依赖项属性的使用,如已经存在的触发器,数据绑定以及样式等。如果要避免对当前使用的清除,那么我们需要使用SetCurrentValue()函数。该函数同样会将当前依赖项属性的属性设置为一个数值,却不会影响当前程序所声明的对它的使用。这是其与SetValue()函数之间的最基本区别。该函数在开发一个自定义控件而言是非常有用的:有时控件内部需要对属性值进行更改,而不去清除用户代码对该属性值的使用。

  接下来,我们就可以在程序中使用依赖项属性Hint了:

<local:AutoCompleteEdit Hint="Please enter text"/>

  创建只读依赖项属性与创建依赖项属性之间略有不同:1) 注册属性时调用RegisterReadOnly方法,而不是Register方法。2) 软件开发人员不需要提供set访问符。3) 只读属性注册返回的是DependencyPropertyKey。而且按照标准做法,类型所记录的DependencyPropertyKey不该是一个具有公有访问权限的成员。软件开发人员应该暴露其所记录的DependencyProperty。下面就是实现只读属性HasHint的代码:

private static readonly DependencyPropertyKey HasHintKey = 
    DependencyProperty.RegisterReadOnly("HasHint", typeof(bool), 
        typeof(AutoCompleteEdit), new FrameworkPropertyMetadata(false),
        new ValidateValueCallback(IsHasHintValid));
public static readonly DependencyProperty HasHintProperty = 
    HasHintKey.DependencyProperty;
public bool HasHint { get { return (bool)GetValue(HasHintProperty); } }

  由于无法对属性值进行设置,WPF的只读依赖项属性无法完成如动画等功能的支持。但是它常常作为一些其它功能的重要支持方式。如ContentControl就为它的Content属性添加了HasContent属性,以允许WPF程序在触发器、绑定等众多功能中将其作为输入。

  在下一篇文章中,我们将对属性更改回调进行讲解。

  转载请注明原文地址:http://www.cnblogs.com/loveis715/p/4343330.html

  商业转载请事先与我联系:silverfox715@sina.com,我只会要求添加作者名称以及博客首页链接。

posted @ 2015-03-16 23:29  loveis715  阅读(2406)  评论(12编辑  收藏  举报