WPF - 属性系统 (4 of 4)

依赖项属性的重写

  在基于C#的编程中,对属性的重写常常是一种行之有效的解决方案:在基类所提供的属性访问符实现不能满足当前要求的时候,我们就需要重新定义属性的访问符。

  但对于依赖项属性而言,属性执行逻辑的重新定义并不能存在于CLR属性包装中:WPF内部对依赖项属性的实现要求依赖项属性的CLR包装实现仅仅调用GetValue()以及SetValue()属性,而不能提供其它的自定义逻辑。相反地,我们需要通过更改创建时所传入的元数据来指定自定义属性执行逻辑,甚至在某些更苛刻的要求下,如更改依赖项属性的类型,重新定义一个具有相同名称的依赖项属性。

  对一个依赖项属性的重写非常简单。如果一个类型从其基类中继承了一个依赖项属性,那么软件开发人员可以在派生类中通过OverrideMetadata()方法完成对属性元数据的覆盖。在重写元数据的时候,系统会将新的元数据与之前的依赖项属性元数据中的各信息进行合并或替换:

  1) 合并PropertyChangedCallback。

  2) 替换DefaultValue。

  3) 替换CoerceValueCallback的实现。

  4) 合并ValidationCallback。

  5) 对于FrameworkPropertyMetadata而言,FrameworkPropertyMetadataOptions的标志组合为按位或运算。

  这里所提到的操作主要分为两种:合并和替换。在这里,合并的意思就是在类型的继承层次中的所有对该组成的赋值都将会被保留。在需要执行该组成的时候,WPF属性系统会按照类型的继承层次依次调用该组成。而替换则表示当前对该组成的声明将会完全替换其所有基类中所声明的该组成。接下来,WPF属性系统仅仅会调用类型继承层次中最高层次的类型所声明的该组成。

  现在我们来看看元数据中各个组成采取合并或是替换的理由。首先要讨论的就是PropertyChangedCallback。该回调所做的事情就是在一个依赖项属性发生了更改的时候刷新其它该类型所包含的依赖项属性。当然,这种逻辑在依赖项属性声明的类型中实现是最正常的一种想法:在同一个类型中调用该函数,刷新其它依赖项属性值可以保证该类型实例处于正常的状态。

  在属性发生更改的时候,系统将首先调用最高层次派生类中所设置的PropertyChangedCallback回调,并沿类型层次结构依次调用各基类实现所提供的各个回调。就像前几节中的实例代码所展示的那样,这些回调常常通过调用CoerceValue来完成其它相关联依赖项属性的更新。通过这一系列回调,该类型继承层次中的各个类型都将处于一个正常的状态。

  对DefaultValue的替换则非常容易理解:由于一个属性不能同时拥有多个默认值,因此使用新的默认值替换基类中所声明的默认值是一种非常正常的选择。

  接下来则是CoerceValueCallback。在依赖项属性发生变化的时候,属性系统将仅调用最直接元数据的CoerceValueCallback。这是因为基类中的CoerceValueCallback回调并不了解派生类中的各个属性,因此一旦定义了新的CoerceValueCallback回调,基类中所定义的逻辑将不再适合对依赖项属性的值进行约束。

  下一个需要讨论的组成则是ValidationCallback回调。由于该函数是在属性注册时传入的,而不是作为元数据中所储存的数据存储在属性系统中。因此它无法被新的属性注册所覆盖。同时不将其添加到元数据中的理由:万一覆盖了,那还需要将所有原ValidationCallback回调中的逻辑重写一遍。

  最后一个需要说明的则是元数据选项的处理。在通过OverrideMetadata()方法操作一个元数据所记录的各个元数据选项的时候,所有的元数据选项将被合并。当然,这里有一种情况就是消除之前设置的元数据选项。在需要达到该目的的时候,我们需要将该元数据选项所对应的属性设置为false。举例来说,软件开发人员可以在元数据中通过NotDataBindable标记设置一个依赖项属性不能被绑定。但是如果需要通过OverrideMetadata()函数清除该选项的时候,软件开发人员就需要在传入的元数据上将IsNotDataBinable属性设置为false。

  当然,OverrideMetadata()函数仅仅是一种重用原有依赖项属性的方法。另一种重用的方法则是AddOwner()。该函数将其它类型中的依赖项属性添加到当前类型中。该函数的签名如下:

public DependencyProperty AddOwner(Type ownerType, PropertyMetadata typeMetadata);

  该函数用来将一个DependencyProperty添加到ownerType所表示的类型上,并可以通过typeMetadata更改该依赖项属性的行为。

  在使用标示依赖项属性的DependencyProperty类型的标记时,我们最好使用AddOwner()函数所返回的依赖项属性标记,而不是原注册类型中所保存的依赖项属性标记。这样做的最主要目的更多是基于语义的考虑。实际上,通过原本的依赖项属性标记以及AddOwner()所返回的依赖项属性标记进行操作所返回的运行结果是相同的。

  与OverrideMetadata()函数明显不同的是,该函数并不继承原属性的元数据。因此在使用AddOwner()函数时,软件开发人员最需要考虑的事情就是是否需要自行指定新属性的元数据。当然,如果软件开发人员对基类的依赖项对象调用AddOwner,那么元数据将被继承并和新元数据合并。

 

引用类型的依赖项属性

  实现一个引用类型的依赖项属性与实现普通的依赖项属性的步骤并没有什么不同:定义一个CLR属性包装,并在该属性包装中通过GetValue()以及 SetValue() 函数完成对依赖项属性值的获取和设置。唯一一点不同的是,软件开发人员不应该在依赖项属性注册的时候为该依赖项属性提供一个默认值,而是在类型的初始化函数中为该依赖项属性显式地赋值。

  为什么要这样做呢?这是因为在这种情况下,多个实例上的引用类型依赖项属性可能会返回一个相同的引用类型实例。产生该问题的原因是由依赖项属性的两个特性共同作用产生的:1. 在没有经过赋值的情况下,一个依赖项属性所返回的值就是在依赖项属性注册时传入的默认值。2.在依赖项属性注册过程中所传入的值实际上是引用类型实例的引用,并将作为所有该依赖项属性的默认值,指向同一个引用类型实例。

  因此在实现一个引用类型的依赖项属性时,我们需要在构造函数中显式地为该引用类型的依赖项属性赋值。在这种情况下,您有两种选择:首先查看依赖项属性的类型是否自定义类型,并可以由class更改成为struct。如果不能,那么在依赖项属性注册过程中将默认值标为null,而在构造函数中再将其设置为所需要的默认值。

  第一种方法在WPF实现中非常常见。就以Control类的Padding属性为例:

public static readonly DependencyProperty PaddingProperty = DependencyProperty
    .Register("Padding", typeof(Thickness), typeof(Control),
        new FrameworkPropertyMetadata(new Thickness(),
        FrameworkPropertyMetadataOptions.AffectsParentMeasure));

  上面的代码注册了一个类型为Thickness的依赖项属性PaddingProperty,并在该属性的元数据中传入了一个默认值。在查看Thickness类型的定义后可以发现,其实际上是一个结构体。在C#中,结构体会在栈上被分配,从而避免了多个该属性所在UI元素引用同一个引用类型实例的情况。

  但事情不能总是这么幸运。首先,依赖项属性的类型可能并不是一个用户自定义类型,因此我们并没有机会将其转化为结构体。另外,一个类型所包含的信息可能非常多,在那种情况下,将一个类型实现为结构体是并不合适的。因此在必须创建一个引用类型的依赖项属性时,我们需要在构造函数中对该属性分别赋值。例如ItemsControl就提供了一个ItemsPanel依赖项属性。如果该依赖项属性通过构造函数进行初始化,那么创建依赖项属性的函数调用以及构造函数定义将如下代码所示:

public static readonly DependencyProperty ItemsPanelProperty = DependencyProperty
    .Register("ItemsPanel", typeof(ItemsPanelTemplate), typeof(ItemsControl),
        new FrameworkPropertyMetadata(null, ……));

public ItemsControl()
{
    SetValue(ItemsPanelProperty, new StackPanel());
}

  但是这违反了WPF对于依赖项属性容器类型构造函数定义的最佳实践。在一个依赖项属性的注册过程中,以及在派生类对该属性的覆盖过程中,软件开发人员都可以为依赖项属性设置回调函数。同时在每次依赖项属性发生变化的时候,这些回调函数都将被执行。由于这些回调函数是在基类的构造函数中被触发,但其所调用的函数可能被派生类重写,所以这些函数的执行可能处于派生类并没有完全初始化的情况。

  为了避免这种问题,WPF提出了一个定义安全的构造函数的标准:

  1. 为您的类型提供一个默认构造函数:

  public MyClass : SomeBaseClass {

      public MyClass() : base() {

          // 所有成员的初始化,包括其它构造函数可能赋值的数据成员或回调函数

          // 将会使用的数据成员

      }

  }

  2. 如果一个类型提供了非默认构造函数,那么该构造函数首先需要调用该类型的默认构造函数,然后再使用SetValue()等函数设置各依赖项属性的值。

  public MyClass : SomeBaseClass {

      public MyClass(object toSetProperty1) : this() {

          // 注意,这里调用的是默认构造函数,而不是基类的构造函数

          Property1 = toSetProperty1;

      }

  }

  只是谁又能保证用户都熟知这些规则并在编写自定义类型的时候按照这些规则对类型进行编写呢?

  如果依赖项属性的类型是一个集合,那么另外一点需要注意的地方则是:XAML解析器无法知道如何调用一个泛型函数。也就是说,如果一个依赖项属性的类型是List<T>,那么WPF并不知道如何调用List<T>.Add(T item),而只知道如何调用非泛型接口成员。因此可知如果软件开发人员希望一个属性是一个集合,那么该集合类型需要实现非泛型的IList接口,如Collection<T>或List<T>。

  而在实现一个集合类型的属性时,到底是将其实现为一个只读依赖项属性还是可读写依赖项属性则会影响该属性在XAML中的使用方法。就以下面两种XAML标记为例:

<Toolbar>
  <Toolbar.Items>
    <ToolbarItem .../>
  </Toolbar.Items >
</Toolbar>

<Toolbar>
  <Toolbar.Items>
    <ToolbarItemCollection>
      <ToolbarItem/>
    </ToolbarItemCollection>
  </Toolbar.Items>
</Toolbar>

  当然,上面的XAML代码仅仅是用作示例,而并非是实际的WPF代码。假设这里的Toolbar类型拥有一个Items属性,其用来记录所有的ToolbarItem类型的子元素。在XAML分析第一段XAML的时候,WPF将首先调用Toolbar.Items属性的get访问符,并依次将该段XAML中所声明的子元素添加到Items属性所记录的集合中。而在分析第二段XAML的时候,WPF将首先创建一个ToolbarItemCollection,并将所有的子元素添加到该集合之中。在该集合创建完毕之后,WPF将调用Toolbar.Items属性的set访问符,以将该集合设置为Toolbar.Items属性的值。

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

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

posted @ 2015-03-19 22:37  loveis715  阅读(1206)  评论(0编辑  收藏  举报