说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
类型转换器
上篇文章中我们已经与类型转换器有了第一次接触,我们看到通过类型转换器将字符串转换为整形。.NET类型转换器(TypeConverter)用于扩展XAML的表达能力,其可以在运行时计算类型为字符串的Attribute的值,生成一个适当的类型的对象。通俗的说就是完成字符串类型与属性本来的类型之间的转换。我们通过例子阐释一下这个抽象的概念:
对比一下如下两段分别用C#与XAML语言描述的功能等价的代码(省略去前一篇中介绍的命名空间的引用):
C#
Button b = new Button(); b.Content = "OK"; b.Background = System.Windows.Media.Brushes.White; |
XAML
<Button Content="OK" Background="White" /> |
仔细观察这段代码,或者你看过前文,你就会发现,在XAML中设置Background属性使用了一个字符串,而在C#中设置这个Background属性时使用了Brushes的属性White(类型为System.Windows.Media.SolidColorBrush)。XAML解析器处理这个问题时,会寻找一个知道如何将一个字符串转换为一种预期的数据类型的类型转换器。
WPF/Silverlight提供了许多类型的类型转换器,如针对Brush(BrushConverter), Color(ColorConverter), FontWeight, Point等的转换器。你也可以为自定义的类型编写类型转换器。类型转化器通常不区分字符串的大小写。
XAML中设置给XML Attribute的字符串知道怎样选择适当的类型转换器来处理字符串。其原理在于,这些属性如上文提到的BackgroundProperty,还有WidthProperty、HeightProperty等都被标记为TypeConverterAttribute来表明这个属性需要类型转换器来处理。另外像是有些属性Stroke,Fill等,虽然没有标记为TypeConverterAttribute,但是他们的基类Brush标有此Attribute以表明需要使用BrushConverter来转换。
例如:
<Rectangle Width="100" Height="20" Stroke="Black" Fill="VerticalGradient Black Red"/> |
Stroke属性交由BrushConverter处理后将得到相关的SolidColorBrush。而Fill属性将会由BrushConverter建立一个渐变画刷,其C#等价代码如下:
Rectangle r = new Rectangle(); r.Fill = new LinearGradientBrush(Colors.Black, Colors.Red, 90.0); |
如果设置非标准类型则需要提供自定义的转化器,如前文所述。
属性元素
在传统的设置对象属性的方式中,如对象属性是一种简单类型的对象,像将一个Button的Height设置为30,在XAML中可以使用如下XML Attribute的方式表示:
<Button Height="30"></Button> |
但是当一个属性可以并需要接受一个复杂的类型的对象,如需要设置Button的Content属性为一个Rectangle对象时。我们就无法继续使用XML Attribute这种较为简洁的写法,在XAML中我们可以使用名为属性元素的语法来完成。示例代码:
<Button> <Button.Content> <Rectangle Height="140" Width="40" Fill="Black"/> </Button.Content> </Button> |
Content属性被设置为一个XML Element而不是XML Attribute。这也是属性元素这个名称的由来。属性元素以"类型名.属性名"形式表示,如Button.Content,其中"."分割对象名与属性名。这个表示类型属性的名称将作为元素出现,且这个元素没有自己的特性。
上述XAML属性元素的表示,与如下所示的C#代码作用相同:
System.Windows.Controls.Button b = new Button(); System.Windows.Shapes.Rectangle r = new Rectangle(); r.Width = 40; r.Height = 40; r.Fill = System.Windows.Media.Brushes.Black; b.Content = r; |
属性元素中也可以设置简单的属性值,如下:
<Button> <Button.Content> OK </Button.Content> </Button> |
上面这种写法只是来演示属性元素,实际应用中还是会以如下传统Attribute的方式书写:
<Button Content="OK" ></Button> |
应用:属性元素多用于设置Content,Background等接受复杂对象的属性。
最后给出一个设置Background属性的属性元素应用的示例:
<Button> <Button.Background> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <LinearGradientBrush.GradientStops> <GradientStop Offset="0" Color="#800"/> <GradientStop Offset="0.35" Color="Red"/> <GradientStop Offset="1" Color="#500"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Button.Background> </Button> |
使用属性元素表示某些属性,也可以很直观地看出具体将使用哪种类型转换器来转换。
附加属性
附加属性是XAML支持而传统.NET语言不支持的一种属性设置方式。附加属性是一种被不同类型定义的属性,也就是说设置这个属性的类型不同于定义这个属性的类型。在WPF/XAML中,许多布局元素的属性常采用这种方式来设置,其定义在父元素中,并在子元素中被设置。附加属性扩展布局系统,用来设定布局风格细节的属性都是附加属性。如:Grid.Row、DockPanel.Dock及Canvas.Left。这样子元素可以使用父元素中定义的附加属性来定位,从而免去了为每个子元素定义布局属性。使用一组对应的布局属性(通过为每个附加属性定义的类中编写一个静态的SetPropertyName方法),就可以定义自己的新布局系统。
语法:
附加属性语法很简单,形如:"DefiningType.PropertyName"这种形式。这个DefiningType与这个属性被设置的类是不同的类型。在内部,XAML通过调用DefiningType.SetPropertyName()这个静态方法完成对属性的设置。
示例:
XAML
<Button Grid.Row="1" x:Name="myButton" /> |
C#
Grid.SetRow(myButton, 1); |
往深里说,附加属性是依赖属性(本系列后面的文章有介绍)的一种特殊形式,所以其可以被有效的添加到任何对象。再看一个例子:现在要给StackPanel这个容器元素中的子元素设置统一的字体与字号。按照我们的经验,自然会想要给StackPanel设置FontStyle与FontSize依赖属性,其子元素会继承这些属性的设置。但是问题是StackPanel没有提供这两个属性。这时就可以使用附加属性来帮忙。我们使用定义于TextElement下的FontStyle与FontSize依赖属性,并把其设置到<StackPanel>元素上。
XAML原生支持附加属性,示例代码如下:
<StackPanel TextElement.FontSize="30" TextBlock.FontStyle="Italic" Orientation="Horizontal" HorizontalAlignment="Center"> <Button MinWidth="75" Margin="10">OK</Button> </StackPanel> |
等价C#代码:
StackPanel panel = new StackPanel(); TextElement.SetFontSize(panel, 30); TextElement.SetFontStyle(panel, FontStyles.Italic); panel.Orientation = Orientation.Horizontal; panel.HorizontalAlignment = HorizontalAlignment.Center; Button okButton = new Button(); okButton.MinWidth = 75; okButton.Margin = new Thickness(10); okButton.Content = "OK"; panel.Children.Add(okButton); |
由C#代码可以看出,附加属性提供程序(附加属性所处的类型) – TextElement依靠其两个静态方法完成设置相应的属性值。对比上文XAML与C#代码,可以看出FontStyles.Italic简写为Italic等这种情况。这是依靠前文提到过的类型转换器。这里用到的是EnumConverter这种类型转换器,这种转换不区分XAML中设置的字符串的大小。
进一步探秘:在内部,SetFontSize方法调用了DependencyObject.SetValue方法。(依赖属性提供器中的SetXXX与GetXXX方法,当然只能调用SetValue与GetValue方法)。
大概实现如下(具体细节等后文介绍了依赖属性后会更明白):
public static void SetFontSize(DependencyObject element, double value) { element.SetValue(TextElement.FontSizeProperty, value); }
public static double GetFontSize(DependencyObject element) { return (double)element.GetValue(TextElement.FontSizeProperty); } |
剩余的疑问,如上例,附加属性(FontSize)并未定义于Button等控件中,而是定义于TextElement中。接下来解释下TextElement.FontSizeProperty这个依赖属性怎样与Control.FontSizeProperty这个依赖属性相关联的。关键在于这两个依赖属性的内部注册方式。
如下是TextElement类中定义的FontSizeProperty依赖属性的源代码:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached( "FontSize", typeof(double), typeof(TextElement), new FrameworkPropertyMetadata( SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(TextElement.IsValidFontSize) ); |
其中RegisterAttached方法对附加属性的属性元数据的处理做了优化。在Control中,不能注册FontSize依赖属性,而调用TextElement已经注册的依赖属性的AddOwner方法,获得对同一实例的引用,代码是这样的:
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits)); |
由以上分析可以看出,控件中的FontSize.FontStyle属性等与字体相关的依赖属性都是由TextElement提供的属性。
自定义附加属性
在过程代码中我们可以使用如下样式的代码将一个依赖属性作为任意一个不相干类型的附加属性。
Panel.SetValue(TextElement.FontSizeProperty, 30); |
这样就可以在其它元素中使用Panel.FontSize这个附加属性
这是WPF提供的不使用继承来扩展类的属性的方式。与C#3.0扩展方法扩展类的效果一致,但是实现完全不同。
本文完
参考:
《WPF揭秘》