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

商业转载请事先与我联系:[email protected],我只会要求添加作者名称以及博客首页链接。

时间: 2024-10-01 07:30:39

WPF - 属性系统 (4 of 4)的相关文章

WPF - 属性系统 (3 of 4)

依赖项属性元数据 在前面的章节中,我们已经介绍了WPF依赖项属性元数据中的两个组成:CoerceValueCallback回调以及PropertyChangedCallback.而在本节中,我们将对其它元数据属性进行讲解. 首先让我们来看看元数据对默认值的支持.在元数据的构造函数中,软件开发人员可以通过它的defaultValue参数指定该依赖项属性的默认值.如果在元数据中并没有指定依赖项属性的默认值,那么WPF属性系统会自动根据依赖项属性的类型为该依赖项属性指定一个默认值: private s

WPF - 属性系统 (1 of 4)

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

WPF - 属性系统 (2 of 4)

属性更改回调 前一章的示例中,对各个参数的设置都非常容易理解.如果我们仅仅需要创建一个独立的依赖项属性,那么上面所提到的创建依赖项属性的基础知识足以满足需求.但是事情往往并非如此完美.在一个系统中,很少有属性是独立存在的,在WPF这种描述界面组成的类库中更是如此.例如一个属性的取值可能受其它众多属性的限制,或者一个属性值的更改可能导致其它依赖项属性值发生更改. 在WPF的属性系统中,这一切关联关系的维护都是通过元数据以及创建属性时所传入的回调来完成的.在创建一个关联属性的时候,我们可以传入一个属

WPF - 属性系统 - APaas(AttachedProperty as a service)

是的,文章的题目看起来很牛,我承认. 附加属性是WPF中的一个非常重要的功能.例如在设置布局的过程中,软件开发人员就常常通过DockPanel的Dock附加属性来设置其各个子元素所处的布局位置.同样地,在为程序添加一个新的功能时,我们也常常需要创建自定义的附加属性来完成该功能. 附加属性简介 首先,我们要对附加属性有一个简单的认识:什么是附加属性,而为什么WPF提供了附加属性呢? 在WPF中,附加属性用来表示定义在一个类型上,却可以在其它特定类型实例上被使用的属性.由于该属性并非定义在这些实例所

Qt属性系统

The Property System Qt提供一个类似于其他编译器供应商提供的精致的属性系统.然而,作为一个编译器和平台独立的库,Qt并不依赖于非标准编译器特性,如__property 或 [property].Qt解决方案能在支持Qt的平台上与任何标准C++编译器一起工作.它依赖于 Meta-Object System . Requirements for DeclaringProperties 要声明一个属性,在继承Qobject的类中用 Q_PROPERTY()宏. Q_PROPERTY

[Qt入门篇]5 Qt的属性系统——声明属性

[Qt入门篇]5 Qt的属性系统--声明属性 Qt提供了灵活的属性系统,它基于Qt的元对象系统,不依赖于编译器,这保证了Qt独立于编译其和平台的特点.这篇文章主要看看如何声明属性. 属性系统比较复杂,先看一个简单的例子.在QWidget中,有很多属性的声明,找一个简单学习: Q_PROPERTY(boolmodalREADisModal) 这里出现了5个元素:Q_PROPERTY.bool.modal.READ.isModal.这五个元素都是啥作用呢? Q_PROPERTY:用于声明属性的宏:

Qt属性系统(Qt Property System)

Qt提供了巧妙的属性系统,它与某些编译器支持的属性系统相似.然而,作为平台和编译器无关的库,Qt不能够依赖于那些非标准的编译器特性,比如__property 或者 [property].Qt的解决方案能够被任何Qt支持的平台下的标准C++编译器支持.它依赖于元对象系统(Meta_Object Sytstem),元对象系统通过信号和槽提供了对象间通讯的机制. 怎样声明属性 QObject的子类的私有域中使用Q_PROPERTY宏来声明一个属性 Q_PROPERTY(type name (READ 

Android6.0 属性系统

属性在android中非常重要,我们基本的不多介绍了,主要说下其用法,原理等. 一.java层获取属性 在java层主要通过SystemProperties这个类来访问Android的系统属性,通过一系列的native函数. public class SystemProperties { ...... public static String get(String key) { if (key.length() > PROP_NAME_MAX) { throw new IllegalArgume

虚幻4属性系统(反射)翻译

反射是程序在运行时进行自检的一种能力.它非常有用且在虚幻引擎中基础技术,支撑了诸如 编辑器中的细节面板.序列化.垃圾回收.网络复制.以及蓝图与C++交互等功能.然而,C++原生并不支持任意形式的反射,因此 虚幻引擎有它自己的系统用来 利用.查询以及操作关于C++类.结构体.函数 .成员变量以及枚举的信息.我们特意把反射叫做属性系统,因为反射也是一个图形术语. 反射系统是可以选择加入的.你需要给暴露给反射系统的类型或属性添加注解,这样Unreal Header Tool (UHT)就会在编译工程的