WPF - 属性系统 - APaas(AttachedProperty as a service)
是的,文章的题目看起来很牛,我承认。
附加属性是WPF中的一个非常重要的功能。例如在设置布局的过程中,软件开发人员就常常通过DockPanel的Dock附加属性来设置其各个子元素所处的布局位置。同样地,在为程序添加一个新的功能时,我们也常常需要创建自定义的附加属性来完成该功能。
附加属性简介
首先,我们要对附加属性有一个简单的认识:什么是附加属性,而为什么WPF提供了附加属性呢?
在WPF中,附加属性用来表示定义在一个类型上,却可以在其它特定类型实例上被使用的属性。由于该属性并非定义在这些实例所对应的类型之上,而更像是为这些类型额外地添加了一系列值,因此其被称为附加属性。
一种较好的理解附加属性这种行为的方式则是将其当作一个服务。其中子元素通过设置这些附加属性的值来定义如何使用服务。而定义附加属性的类型将根据这些子元素中设置的数值决定其所提供服务的方式,如一个控件应该放置在哪里。这种使用方法也便是决定我们是否创建一个附加属性的最重要因素。
而实现一个附加项属性则非常简单。首先,软件开发人员需要通过调用DependencyProperty类的RegisterAttached()函数在属性系统中声明一个附加属性。与依赖项属性不同的是,由于其并非作用于定义该附加属性的类型上,因此无法提供CLR属性包装,而是提供了以Get-以及Set-作为属性名前缀的专用方法。这些专用方法中,软件开发人员可以通过规定参数以及返回值的类型等方式限制使用该附加属性的类型以及该属性的值。
现在就以.net源码中的DockPanel.Dock附加属性的实现来熟悉创建附加属性的过程:
public static readonly DependencyProperty DockProperty = DependencyProperty .RegisterAttached("Dock", typeof(Dock), typeof(DockPanel), ……); public static Dock GetDock(UIElement element) { if (element == null) { throw new ArgumentNullException("element"); } return (Dock) element.GetValue(DockProperty); } public static void SetDock(UIElement element, Dock dock) { if (element == null) { throw new ArgumentNullException("element"); } element.SetValue(DockProperty, dock); }
首先,软件开发人员需要通过DependencyProperty类的RegisterAttached()以及RegisterAttachedReadonly()函数来向属性系统中注册附加属性。这两个函数所使用的各个参数与Register()函数所使用的各个参数类似:通过参数name指定附加属性的名称;通过propertyType参数指定附加属性的类型;通过ownerType参数指定附加属性所在的类型;通过defaultMetadata参数指定附加属性所使用的元数据;最后通过validateValueCallback指定验证逻辑。而在附加属性的承载类型中,软件开发人员需要通过一个静态公有的DependencyProperty来记录这些新注册的附加属性。这个附加属性的属性名需要遵守一定的规则:其需要由在调用RegisterAttached()函数时所传入的参数name以及后缀-Property组合而成。接下来,软件开发人员就需要定义用来访问附加属性的读写函数。这些函数的名称则需要通过前缀Get-和Set-以及附加属性注册时所传入的参数name共同组成。在这些函数内部,软件开发人员只需要调用DependencyObject的GetValue()和SetValue()即可。
让我们来回想一下WPF中DockPanel的使用过程:软件开发人员首先在XAML中声明了一个DockPanel,并在该其中声明了众多的子元素,以表示需要承载在该DockPanel中的界面组成。在这些界面组成的声明中,我们可以使用DockPanel.Dock属性来指定每个子元素需要放置在DockPanel的哪个位置。整个过程如下面代码所示:
<DockPanel LastChildFill="True"> <Border DockPanel.Dock="Top" ...> <TextBlock Foreground="Black">Dock = "Top"</TextBlock> </Border> ... </DockPanel>
在XAML编译器遇到对DockPanel.Dock属性的赋值时,其会将该赋值转化为对DockPanel.SetDock()函数的调用。该函数会以包含DockPanel.Dock属性赋值的界面元素以及被赋予的值作为参数。而在执行布局计算的时候,DockPanel会使用GetDock()函数将所有子元素的DockPanel.Dock附加属性值读取出来,并根据这些值安排各个子元素所处的位置。这就相当于DockPanel提供布局计算的服务,而每个子元素则通过附加属性DockPanel.Dock提供了自身所需要的服务的类型。
另外需要强调的一点则是附加属性的Get-和Set-函数中对能使用附加属性的类型的限制。在DockPanel所提供的GetDock()和SetDock()函数中,第一个属性的类型是UIElement,也就表示DockPanel只为UIElement提供附加属性的服务。这其实也是附加属性限制其所可施行类型的最常用方法:在Get-和Set-函数的定义中将第一个参数的类型设置为附加属性的目标类型,从而限制其它类型对该附加属性的使用。
同理,软件开发人员也可以为Get-函数的返回值以及Set-函数的参数value指定一个特定的类型,以防止用户代码为附加属性设置一个其它类型的值。其内部实现会直接调用传入的参数的GetValue()和SetValue()函数来完成附加属性的设置。这两个函数并没有以自身实例作为参数。也就是说,附加属性并没有设置在声明该附加属性的类型实例上,而是设置在了使用附加属性的实例上。
从上面的讲解中可以看出,附加属性所对应的Get-和Set-函数内部并没有引用任何宿主类型成员,而是将属性值设置到了传入的参数上。因此在定义一个附加属性的时候,依赖项属性的宿主类型不必从DependencyObject类派生。
如果需要让附加属性所对应的服务能够正确执行,仅仅限制附加属性的目标类型是不够的。附加属性所提供的服务常常需要按照一定的方式搜索其所在类型之下的各个子元素,以搜集它们所包含的有关服务的信息,并根据这些信息提供服务。就以DockPanel类所提供的DockPanel.Dock附加属性为例:
protected override Size ArrangeOverride(Size arrangeSize) { UIElementCollection internalChildren = base.InternalChildren; int count = internalChildren.Count; for (int i = 0; i < count; i++) { UIElement element = internalChildren[i]; if (element != null) { Size desiredSize = element.DesiredSize; Rect finalRect = … // 子元素最终所处的布局位置 switch (GetDock(element)) { case Dock.Left: x += desiredSize.Width; finalRect.Width = desiredSize.Width; break; case Dock.Top: // 依次处理Dock.Top,Dock.Right以及Dock.Bottom等情况 } element.Arrange(finalRect); } } return arrangeSize; }
上面的示例代码非常好地展示了一个附加属性是如何在类型中被使用的:在通常情况下,包含附加属性的类型常常包含了一个集合,以记录其所包含的各个子元素。在特定逻辑运行过程中,该集合内所记录的所有子元素将被遍历。在每次遍历中,执行逻辑都会取得子元素的附加属性值,并以此属性值来决定子元素的具体行为。
因此,如果这些信息并没有按照正确的方式提供,那么附加属性所提供的服务也可能不被正确运行。就以下面的代码为例:
<DockPanel LastChildFill="True"> <Border DockPanel.Dock="Top" ...> <TextBlock DockPanel.Dock=”Bottom” Foreground="Black">Dock = "Top"</TextBlock> </Border> ... </DockPanel>
在上面的代码中,DockPanel的Border类型的子元素以及Border所包含的TextBlock元素都设置了DockPanel.Dock附加属性。但是由于DockPanel仅仅排列其所包含的各个子元素,而这些子元素所各自包含的内嵌元素的布局则是由各个子元素自己决定,因此DockPanel仅仅对它的直接子元素的DockPanel.Dock附加属性进行处理。所以在上面的例子中,TextBlock元素所设置的DockPanel.Dock附加属性将不会被处理。
附加属性相关特性
现在来想想附加属性在XAML中的使用。在编写XAML标记的时候,Visual Studio会根据当前输入提示可用的各属性。这其中就有附加属性:
但是这里就出现了问题:该列表中并没有DockPanel.Dock附加属性,却只有Grid相关的附加属性。这是为什么呢?查看有关附加属性的实现时,我们发现了一些端倪:
[AttachedPropertyBrowsableForChildren] public static int GetColumn(UIElement element) { if (element == null) { throw new ArgumentNullException("element"); } return (int) element.GetValue(ColumnProperty); }
在上面的代码中,我们可以看到Column附加属性的Get-访问函数上添加了一个特性AttachedPropertyBrowsableForChildren。查看MSDN可以知道,该属性用来标示当前附加属性可以在逻辑树中的子元素中被使用。其包含两种不同的子元素查询方法:在没有显式地将IncludeDescendants属性标为true的时候,依赖项属性将仅仅在子元素中可见。而在将IncludeDescendants属性标为true的时候,该依赖项属性可以在相应元素中的任何子元素中出现。一般情况下,该特性只在Get-访问符上施行。
在定义一个附加属性的时候,我们的确可以通过标示这些特性使得一个附加属性可以在一个元素的属性列表中出现。但是该附加属性的设置是否被处理则是由附加属性的实际执行逻辑所决定的。因此软件开发人员在使用该特性使一个附加属性在某个元素中出现的时候,您首先需要尽量保证处理逻辑能处理该特性所标示的附加属性可见范围。
另一个较为有用的特性就是AttachedPropertyBrowsableForType。该特性允许软件开发人员指定一个附加属性只对特定类型可见。在需要令附加属性对多种类型可见的时候,软件开发人员需要在附加属性的Get-访问符上添加多个该类型的特性。
而最后一个特性就是AttachedPropertyBrowsableWhenAttributePresentAttribute。该特性表示只有宿主类型上标明了特定特性时,该依赖项属性才可见。这并不是一个非常常用的特性,因此我们将不在这里对其进行讲解。
附加属性的绑定
从上面的讲解中您已经了解到,附加属性实际上就是一个依赖项属性,因此其同样可以作为绑定的数据源使用。但是此时绑定所使用的标记则与普通的依赖项属性有所不同。例如在需要绑定到静态实例local:StaticClass的Button属性所设置的Grid.Row附加属性的时候,软件开发人员需要使用圆括号将附加属性括起:
{Binding Source={x:Static local:StaticClass}, Path=Button.(Grid.Row)}
使用不同标记的原因则非常简单:XAML解析器需要根据不同的标记来判断绑定表达式中的各个组成到底是对依赖项属性还是附加属性的引用。
转载请注明原文地址:http://www.cnblogs.com/loveis715/p/4343381.html
商业转载请事先与我联系:silverfox715@sina.com,我只会要求添加作者名称以及博客首页链接。