附加属性
附加属性一种特殊的依赖属性形式,它可以被附加到任意对象上。
对于之前的About Dialog示例,如果我们不想让整个Window元素及其子元素都被FontSize和FontStyle影响,而是希望改变它们仅影响位于第二个StackPanel中的OK和Help两个按钮。我们很自然地会想到把它们从Window元素中移动到StackPanel元素中,但这样做是不可以的,因为StackPanel本身并不包含任何与字体相关的属性。此时,我们可以使用定义在TextElement类中的附加属性FontSize和FontStyle来完成相同的任务:
<Window x:Class="Test.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF揭秘" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF揭秘(版本3.0)
</Label>
<Label>(C)2006 SAMS 出版集团</Label>
<Label>已安装的章节:</Label>
<ListBox>
<ListBoxItem>第一章</ListBoxItem>
<ListBoxItem>第二章</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"
TextElement.FontSize="30" TextElement.FontStyle="Italic">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>
<Button>您已经注册了本产品。</Button>
</StatusBar>
</StackPanel>
</Window>
TextElement.FontSize和TextElement.FontStyle必须用在StackPanel元素上,因为它不包含这两个属性。当XAML解析器/编译器遇到这个语法时,需要TextElement(有时被称作“附加属性提供器”)提供静态的SetFontSize和SetFontStyle方法。因此,与上例中第二个StackPanel等价的C#代码为:
StackPanel panel = new StackPanel();
TextElement.SetFontSize(panel, 30);
TextElement.SetFontStyle(panel, FontStyles.Italic);
panel.Orientation = Orientation.Horizontal;
panel.HorizontalAlignment = HorizontalAlignment.Center;
Button helpButton = new Button();
helpButton.MinWidth = 75;
helpButton.Margin = new Thickness(10);
helpButton.Content = "Help";
Button okButton = new Button();
okButton.MinWidth = 75;
okButton.Margin = new Thickness(10);
okButton.Content = "OK";
panel.Children.Add(helpButton);
panel.Children.Add(okButton);
枚举值如FontStyles.Italic、Orientation.Horizontal和HorizontalAlignment.Center可以在XAML中被分别简单表示为Italic、Horizontal和Center,这要感谢.NET中的EnumConverter类型转换器,它可以将任何大小写不敏感的字符串转换为枚举值。
在内部,SetFontSize方法调用了DependencyObject.SetValue方法,后者一般由依赖属性的访问器(accessor)(就是之前说过的按钮的IsDefaultProperty的访问器IsDefault)调用,但此时却是由当前传入的DependencyObject的实例进行调用:
public static void SetFontSize(DependencyObject element, double value)
{
element.SetValue(TextElement.FontSizeProperty, value);
}
相似地,附加属性也定义了调用DependencyObject.GetValue的GetXXX方法:
public static double GetFontSize(DependencyObject element)
{
return (double)element.GetValue(TextElement.FontSizeProperty);
}
附加属性被广泛使用在WPF的布局系统中,相关内容放到以后介绍。
Button等控件继承的FontSize和FontStyle附加属性并不是由它们自己定义的,而是定义在其基类Control中,这十分令人迷惑。实际上,它们真正被定义在看起来与控件并不相关的TextElement类当中。在TextElement类中,包含下面的注册代码:
TextElement.FontSizePropert = DependencyProperty.RegisterAttached(
"FontSize", typeof(double), typeof(TextElement),
new FrameworPropertyMetadata(
SystemFonts.MessageFontSize,
FrameworkPropertyMetadataOptions.Inherits |
FrameworkPropertyMetadataOptions.AffectsRender,
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(TextElement.IsValidFontSize));
与前面讲到的Button.IsDefaultProperty的Register类似,只不过RegisterAttached为附加属性优化了元数据的处理过程。
控件并不注册它们,而是通过调用AddOwner来添加一个已注册了的依赖属性:
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,
FrameworkPropertyMetadata.Inherits));
因此,由控件继承所公开的FontSize、FontStyle以及其它字体相关的依赖属性都与TextElement公开的属性完全一样。所幸在多数类中,公开了附加属性的类(比如包含GetXXX和SetXXX)都与定义了一般依赖属性的类相同,这使得许多混乱得以避免。
与Windows Form类似,WPF中的许多类也定义了一个System.Object类型的Tag属性,用以存储任意与当前实例相关的数据。与Tag属性相比,附加属性是为派生自DependencyObject的对象附加自定义数据的一种更加强大、更灵活的机制。附加属性允许为密封类(sealed)的实例添加自定义数据,WPF中有许多这样的做法,常常被忽略。
尽管在XAML中设置附加属性时需要依靠静态的SetXXX方法,但是我们在程序代码中可以直接通过DependencyObject.SetValue方法完成。这意味着我们可以在程序代码中将任何依赖属性作为附加属性。例如,下例将ListBox的IsTextSearchEnabled属性附加给了Button,并为其设置了一个值:
okButton.SetValue(ListBox.IsTextSearchEnabledProperty, true);
尽管这么做没什么意义,而且它确实对Button不起作用,但是我们仍旧可以一种对程序和组件有意义的方式来自由地使用它。
下例展示了一种有趣的元素扩展方式:FrameworkElement的Tag属性是一个依赖属性,因此我们可以把它附加到GeometryModel3D的实力上。
GeometryModel3D model = new GeometryModel3D();
model.SetValue(FrameworkElement.TagProperty, "自定义数据");
这只不过是一种 WPF 提供灵活性的方式,这种方式不需要继承就可以获得新的属性。