说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
标记扩展的作用同类型转换器(见本系列上一篇文章),都是将字符串转换为相应类型的对象。WPF/Silverlight内建的标记扩展都派生自MarkupExtension。其应用场景如下,当我们想设定一个属性的值为一些特定的静态属性值,但我们在编译时不知道这个值(如一个由用户的配置来决定的颜色),这时候就可以使用标记扩展。简单说标记扩展是一种用来设置属性值的类。
与类型转换器不同的是,标记扩展通过XAML显式的,一致的调用,因此这是更好的扩展XAML的方法。另外标记扩展也可以完成一些类型转换器所不能完成的功能。例如,通过自定义一个标记扩展可以实现使用一个简单的字符串将控件的背景色设置为渐变笔刷,而使用类型转换器是无法完成的。
标记扩展的语法及组成
XAML分析器将由"{ }"括起来的Attribute值认作一个标记扩展。
花括号中第一个标识符被识别符为标记扩展的名称,即定义标记扩展的类的名称,按照惯例这样的扩展常以Extension后缀结尾,但当在XAML中使用时可以省略该后缀。XAML分析器会自动添加并进行进一步处理。
第二个标识符是标记扩展接受的参数。如果标记扩展接受传入参数,可以为其指定参数值,并以逗号分隔各参数。标记扩展接受的参数分为两种:
-
定位参数,其被作为字符串参数传入扩展类的相应的构造函数。
-
命名参数,可以用来在已构造好的扩展对象上设置相应名字的属性。这些属性的值也可以是标记扩展(即标记扩展允许嵌套),也可以是文本值,通过类型转换器在运行时转换为相应的类型。
在XAML编译时,标记扩展的参数将被传入标记扩展类的重载的构造函数中来创建构造函数的一个新实例。在构造函数内部使用ProvideValue方法得到参数表示的实际值并提供给XAML的Attribute(即标记扩展的所服务的特性)。
我们通过下面的例子来看这个标记扩展参数处理过程可由:
<Style TargetType="{x:Type Button}"></Style> |
如上的标记扩展(其中的参数为定位参数),在XAML编译时会使用类似如下的C#代码来给TargetType赋值:
TypeExtension te = new TypeExtension(); object val = te.ProvideValue(s, Style.TargetTypeProperty); |
XAML对扩展标记类的验证有两种方式:编译时验证与运行时验证。XAML编译器挑选出部分扩展标记在编译时进行验证(这些标记扩展对于特定的参数,总是返回相同的值),另外大多数扩展(包括自定义扩展等)都在运行时进行测试。
上面例子中的TypeExtension是属于编译时验证的扩展标记类。验证代码形如:
Style s = new Style(); s.TargetType = typeof(Button); |
标记扩展的设计与.NET Framework的扩展机制 - 特性(Attribute)的设计是一致的。
标记扩展示例:
<Button Background="{x:Null}" Height="{x:Static SystemParameters.IconHeight}" Content="{Binding Path=Height, RelativeSource={RelativeSource Self}}" /> |
在上面的例子中,NullExtension与StaticExtension位于System.Windows.Markup命名空间,所以需要使用x前缀来定位。即x:Null与x:Static,而Binding(无Extension后缀)位于System.Windows.Data命名空间下,在XAML导入的主命名空间中,所以不用使用前缀。关于这些XAML命名空间内容可参见本系列第一篇文章中所介绍内容。
例子中SystemParameters.IconHeight与嵌套标记扩展中Self属于定位参数。而Path与RelativeSource属于命名参数。
StaticExtension允许使用静态属性,字段,常量及枚举值,不使用XAML中硬编码的值(如使用硬编码值则无需使用标记扩展)。
WPF中的标记扩展
WPF提供了一些内置的扩展标记,大部分被定义于XAML的XML命名空间,小部分位于XAML的WPF命名空间,前者需要通过x:访问,后者可以直接访问。下面的列表给出了这些内置标记扩展。
类 型 |
XAML |
用途 |
NullExtension |
x:Null |
用来表示null() |
TypeExtension |
x:Type |
得到Type对象 |
StaticExtension |
x:Static |
得到静态属性值 |
StaticResource |
StaticResource |
执行一次性的资源查找 |
DynamicResource |
DynamicResource |
设定动态资源绑定 |
ArrayExtension |
x:Array |
建立数组 |
Binding |
Binding |
建立数据绑定 |
TemplateBinding |
TemplateBinding |
模板绑定 |
下面逐一分析这些标记扩展:
-
NullExtension
NullExtension提供设置属性为空值的方法。在部分情况下,不设置属性值与显式设置为null的区别很大,尤其是属性已经被设置为某值,这时候设置为空相当于清除之前的设置。
上面的例子中对Button的Background属性的设置就展示了NullExtension的使用,将Background设置为null就可以清除之前所设置的背景,这是一个很好的例子。
-
TypeExtension
TypeExtension将返回一个System.Type对象给标记扩展所服务的Attribute。其接受一个定位参数,表示类型的名称。XAML将通过TypeExtension把这个字符串表示的类型名转化为相应的类型,同时这个类型名也不需要提供其完整命名空间(.NET),默认的命名空间就是该XAML的主命名空间与x:命名空间。
前文给TargetType属性设置值的标记扩展就是TypeExtension的一个例子。
-
StaticExtension
StaticExtension在前文有所提及,其将对象的属性设置为特定的静态值。其接受一个参数,确定属性的来源,参数格式为ClassName.PropertyName。
前文示例中设置Button的Height的代码演示了StaticExtension的使用。
StaticExtension存在的问题在于当属性(Property)变化时不能自动修改属性(Attribute)的值。另外单独使用StaticExtension不能很好的结合系统设置与程序设置。在实际应用中往往将StaticExtension与StaticResource扩展结合使用。
-
StaticResource
StaticResource返回一个指定资源的值。等效于调用元素的FindResource方法。StaticResource与下面将介绍的DynamicResource两个标记扩展位于WPF的命名空间,使用时无须x:前缀。下面是一段示例:
XAML:
<TextBlock Name="myText" Background="{StaticResource {x:Static SystemColors.ActiveCaptionBrushKey}}"/> |
等效C#:
myText.Background = (Brush)myText.FindResource(SystemColors.ActiveCaptionBrushKey); |
这段代码对资源进行一次性查找,属性(Property)值将会在初始化期间被设置为资源值。但资源值的变化不会引起属性值的变化,所以,如当改变系统颜色主题时,元素的背景不会随之更新。有效的解决方法就是使用下面介绍的DynamicResource。
-
DynamicResource
DynamicResource将属性(Property)值与资源值联系起来,其使用方式与StaticResource相似,但可以跟踪资源改变。
<TextBlock Name="myText" Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"/> |
等效的C#:
myText.SetResourceReference(TextBlock.BackgroundProperty, SystemColors.ActiveCaptionBrushKey); |
由代码可以看出,在DynamicResource中,将StaticResource中资源赋值的方式改为设置引用,这样资源的值的改变可以被跟踪。这样当系统资源改变时,控件的背景也会随之改变。
-
ArrayExtension
ArrayExtension用来将元素值设置为一个元素的数组,其需要一个数组类型变量作为指定类型的属性值。由于这种类型的标记扩展所接受的参数往往很长,所以通常其内容不采用"{ }"的形式来表示,而是采用属性元素这种语法,其中每个数组的值都被表现为ArrayExtension元素的子项。(当然对于空数组可以使用"{ }"以使代码更简洁)。参见如下示例:
<Grid> <Grid.Resources> <x:ArrayExtension Type="{x:Type Brush}" x:Key="brushes"> <SolidColorBrush Color="Blue"/> <LinearGradientBrush StartPoint="0,0" EndPoint=" 0.8,1.5"> <LinearGradientBrush.GradientStops> <GradientStop Color="Green" Offset="0"/> <GradientStop Color="Cyan" Offset="1"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> <LinearGradientBrush StartPoint="0,0" EndPoint=" 0,1"> <LinearGradientBrush.GradientStops> <GradientStop Color="Black" Offset="0"/> <GradientStop Color="Red" Offset="1"/> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </x:ArrayExtension> </Grid.Resources>
<ListBox ItemsSource=" {StaticResource brushes}" Name="myListBox"> <ListBox.ItemTemplate> <DataTemplate> <Rectangle Fill="{Binding}" Width="100" Height="40" Margin="2"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> |
等效C#:
Brush[] brushes = new Brush[3]; brushes[0] = Brushes.Blue; brushes[1] = new LinearGradientBrush(Colors.Green, Colors.Cyan, new Point(0, 0), new Point(0.8, 1.5)); brushes[2] = new LinearGradientBrush(Colors.Black, Colors.Red, new Point(0, 0), new Point(0, 1));
myGrid.Resources["brushes"] = brushes; myListBox.ItemsSource = myListBox.Resources["brushes"]; |
这段代码中ArrayExtension中建立一个数组作为资源,数组中的每一项又使用了TypeExtension,从而建立一个Brush类型的资源数组,最后将资源设置给列表框。
-
Binding
Binding标记扩展用来进行数据绑定。示例代码:
<TextBlock Text="{Binding Foo}" x:Name="txt"/> |
这段代码在数据上下文将对象的Text属性绑定到Foo。
BindingOperations.SetBinding(txt, TextBlock.TextProperty, b); |
-
TemplateBinding
模板绑定用在控件模板中,用于将源对象属性映射到模板中的对象属性。示例:
<Rectangle Width="100" Height="200" Fill="{TemplateBinding Background}"/> |
等效C#:
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(Rectangle)); factory.SetValue(Rectangle.WidthProperty, 100); factory.SetValue(Rectangle.HeightProperty, 200); TemplateBindingExpression tb = new TemplateBindingExpression(Button.BackgroundProperty); factory.SetValue(Rectangle.FillProperty, tb); |
模板绑定用在模板的上下文。模板元素使用FrameworkElementFactory来建立其内容。这是因为模板可以被实例化很多次。
"{ }"的"转义"
如果你需要设置的一个属性值的字面值以"{"开头,则需要特殊的方法对其转义,以免其被当作标记扩展处理,转意方法是在"{"之前加上一对"{ }"。
代码示例:
<Button Content="{}{This is not a markup extension!}"/> |
或者使用属性元素实现同样的目的,等价代码:
<Button> {This is not a markup extension!} </Button> |
这段代码使用了隐式属性元素这个语法,这得益于内容属性这种语法。完整写法如下:
<Button> <Button.Content> {This is not a markup extension!} </Button.Content> </Button> |
因为标记扩展是有默认构造函数的类,其可以与属性元素一起使用,前文示例的标记扩展的代码等价于如下XAML:
<Button> <Button.Background> <x:Null/> </Button.Background> <Button.Height> <x:Static Member="SystemParameters.IconHeight"/> </Button.Height> <Button.Content> <Binding Path="Height"> <Binding.RelativeSource> <RelativeSource Mode="Self"/> </Binding.RelativeSource> </Binding> </Button.Content> </Button> |
代码中StaticExtension有一个Member属性与传入标记扩展x:Static的形参的实参含义相同,同理,RelativeSource有一个对应于的其构造函数参数的Mode属性。
自定义标记扩展
通过编写继承自MarkupExtention的类可以创建自己的标记扩展,要确保XAML编译器可以找到你的扩展类型,并将参数恰当的传入,最主要要做的就是提供恰当的重载构造函数来接收参数(适用于定位参数)或建立合适的属性(适用于命名参数)。下面的C#示例代码展示了怎样用标记扩展中的命名参数给属性赋值,通过这可以看出为什么自定义标记扩展时要定义参数。
<TextBlock TextContent="{Binding Path=SimpleProperty, Mode=OneTime}"/> |
C#初始化标记扩展的方法:
Binding b = new Binding(); b.Path = new PropertyPath("SimpleProperty"); b.Mode = BindingMode.OneTime; |
参考:
《WPF揭秘》