这一篇我要总结的内容是XAML中的扩展标记(Markup Extensions).
扩展标记
通过类型转换器和属性元素,我们可以将大多数属性初始化为常数值或者固定结构,不过在某些情况下我们需要更强的灵活性。举个例子,虽然我们可能会设置一个等价于某些特定静态属性的属性,但是我们并不知道在编译时该属性值将等于什么,这就像用来表示用户自定义颜色的属性一样。XAML以扩展标记的形式提供一个强大的解决方案。一个扩展标记就是一个在运行时决定如何设置属性值的类。
扩展标记类派生自MarkupExtension,下面代码显示了其非私有成员。这个类定义在System.Windows.Markup命名空间中。
例1:扩展标记类:
public abstract class MarkupExtension
{
protected MarkupExtension() { }//构造函数,初始化从MarkupExtension派生的类的新实例。
public abstract object ProvideValue(IServiceProvider serviceProvider);//在派生类中实现时,返回一个对象,此对象是作为此标记扩展的目标属性的值提供的。
}
下面是一个关于扩展标记的例子。
例2:使用一个扩展标记:
...
<Style TargetType="{x:Type Button}"/>
...
其中Style的TargetType属性有一个用大括号括起来的值,通过这一点XAML编译器就能识别使用了扩展标记。大括里的第一个字符串是扩展标记类名,接下来的内容将会在初始化时传入扩展标记。
在上例中我们使用了x:Type。虽然由XAML命名空间表示的.NET命名空间中没有任何一个叫做Type的类,但是当XAML编译器找不扩展标记类时,它就会为名称添加Extension并且重试。由于有一个TypeExtension类,因此XAML的编译器会使用这个类,并将字符串"Button"传递给它的构造函数,这样就初始化了一个TypeExtension类的实例。然后,它会调用扩展标记的ProvideValue方法来获得一个用于属性的真实值,而TypeExtension则会返回Type对象供Button类使用。
如例1所示,ProvideValue只拥有一个类型为IserviceProvider的参数。这是一个标准的.NET接口,通过它可以提供一套服务,而每一个服务都是一些接口的实现。这里提供了两个服务:IProvideValueTarget和IXamlTypeResolver.通过前者,扩展标记可以获得所有提供值的对象和属性(这里是Style对象的TargetType属性)。通过后者可以将类型名转换成类型(这里是将Type转换成TypeExtension类),并将使用了扩展标记的位置纳入XML命名空间的范畴。TypeExtension就是通过这个服务将其参数转换成Type对象。
XAML的编译器对特定的扩展标记进行了特殊的处理,并在编译时进行求值,由于编译器作者知道那些扩展标记总是返回同样的值给特定的输入。(大多数扩展标记在运行的时候进行求值,其中包括您所写的任何一个自定义扩展名)。TypeExtension就是其中的一个特殊情况,因此,在编译时,编译器将执行等同于例3所示的代码。(由于传递给ProvideValue方法的服务提供者是一个XAML编译器详细的执行过程,因此这里没有显示出来)。
例3:编译时TypeExtension所产生的效果:
TypeExtension te = new TypeExtension("Button");//初始化TypeExtension的实例
object val = te.ProvideValue(serviceProviderImpl);//调用ProvideValue方法,通过IProvideValueTarget接口获得了Style对象的TargetType属性。
例2中TypeExtension类在运行时所产生的效果等同于例4所示的代码。这种特殊的处理其主要原因还是效率的问题,也就是说由于普遍使用了TypeExtension,因此在运行时查找名称将会降低速度。
例4:TypeExtension的运行时效果:
Style s = new Style();
s.TargetType=typeof(Button);
XAML可以通过两种方式将数据传递进扩展标记。一种就是如例2所示的构造函数参数,其中TypeExtension提供一个可以带有字符串的构造函数,并且那个示例将字符串"Button"传进了构造函数。(可以用逗号分隔来传递多个参数)。另一种是设置属性,您可以通过将PropertyName=value对列成一个清单。如例5.
例5:使用带有扩展标记的Name=Value值对:
<TextBlock Text="{Binding Path=SimpleProperty,Mode=OneTime}"/>
传递给扩展标记的属性也与其他所有属性一样,是使用类型转换器来进行解析的。例5等价于例6所示的代码。
例6:为绑定设置属性:
Binding b = new Binding();
b.Path = new PropertyPath("SimpleProperty");
b.Mode = BindingMode.OneTime;
内置的扩展标记
Silverlight提供了许多实用的内置扩展标记。其中一些定义在XAML XML命名空间中,因此按照惯例您可以使用x:前缀来对它们进行访问。下表是常用的扩展标记。
类型 |
说明 |
x:NullExtension |
用来表示空值(编译时进行求值) |
x:TypeExtension |
获得类型对象(在编译时进行求值) |
x:ArraryExtension |
创建一个数组 |
x:StaticExtension |
获得静态属性值 |
下面就来分别介绍这些内置的扩展标记。
NullExtension
NullExtension提供了一种将属性设置为NULL的方法。在某些情况下显示将属性设置为NULL非常重要。比如一个Style可能会将所有元素的Background属性设置为某一个特定值,而您可能会在某一个指定元素上屏蔽这个值。如果想移除背景色,不是将它设置为另一种颜色,那么就要通过显示将该元素的属性的Background设置为NULL来实现。
下面使用NullExtension设置了一个按钮的背景色,这样可以防止按钮的背景色被填充。
<Button Background="{x:Null}">Click</Button>
以上代码等价于:
Button b = new Button();//创建一个Button类对象
b.Background = null;//设置Background属性
b.Content = "Click";//设置Content属性
TypeExtension
TypeExtension会为已命名的类型返回一个System.Type对象。它往往带有一个参数:那就是类型名。在应用了TypeExtension时,TypeExtension会通过IServiceProvider传递给它的ProvideValue方法来获得有关XAML解析环境的信息,并且使它能够像XAML编译器处理元素名称那样来处理类型名。这就意味着,您不必提供一个完全限定的含有.NET命名空间扩展标记的类型名称,相反您只需按以下方式做就可以。
<Style TargetType="{x:Type Button}"/>
TypeExtension将会把字符串"Button"像XAML编译器一样处理成一个类型,也就是说,它将会被包括进默认的XML命名空间中以及任何存在的命名空间映射中。这个扩展标记的作用等价于:
Style s = new Style();
s.TargetType=typeof(Button);
同样,实际应用过程中会复杂很多,不过上面的例子解释了扩展标记的目的和作用。
ArrayExtension
通过ArrayExtension您可以创建一组元素。这个扩展标记有点特殊,因为该标记不需要使用大括号,也就是说要通过一个完整元素来代替。这是因为一个组可以包括多个选项,并且有了ArrayExtension,这些就表现为扩展标记子元素,(不过如果想获得一个空数组,可以使用大括号来实现)。
下例使用ArrayExtension来创建了一个数组作为资源,接着将这一数组作为一个ListBox的数据源。ArrayExtension必须通过其Type属性来指定数组类型,对于这个属性我们可以使用前面介绍的TypeExtension,下面我们来创建一个Brush类型的数组。
<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>
以上代码等价于以下代码:
Brush[] brushes = new Brush[3];//声明Brush类型的数组,大小为3
SolidColorBrush scb = new SolidColorBrush();//创建SolidColorBrush类对象
scb.Color = Colors.Blue;//设置属性Color
brushes[0] = scb;//初始化数组的第一个元素
LinearGradientBrush lgb = new LinearGradientBrush();//创建对象
lgb.StartPoint = new Point(0,0);//设置属性
lgb.EndPoint = new Point(0.8,1.5);
GradientStop gs = new GradientStop();//创建对象
gs.Color = Colors.Green;//设置属性
gs.Offset = 0;
lgb.GradientStops.Add(gs);//调用Add()方法添加对象
gs = new GradientStop();//再创建一个GradientStop对象
gs.Color = Colors.Cyan;
gs.Offset = 1;
lgb.GradientStops.Add(gs);
brushes[1] = lgb;//实始化数组的第二个元素
lgb = new LinearGradientBrush();
lgb.StartPoint = new Point(0,0);
lgb.EndPoint = new Point(0,1);
gs = new GradientStop();
gs.Color = Colors.Black;
gs.Offset = 0;
lgb.GradientStops.Add(gs);
gs = new GradientStop();
gs.Color = Colors.Red;
gs.Offset = 1;
lgb.GradientStops.Add(gs);
brushes[2] = lgb;//实始化数组的第三个元素
myGrid.Resources["brushes"] = brushes;
...
myListBox.ItemsSource=myListBox.Resources["brushes"];
结果如图所示:
注意这个示例可能不是使用标记的最佳选择,不过对于创建一个等价的数组来说,代码还可以编写得更简洁一些。
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));
如果使用标记或代码都可以实现您所要的功能,使用代码往往会更简洁一些,而ArrayExtension存在的主要原因是得益于基于XAML的工具。通过ArrayExtension,这些工具就可以创建一个数组而不需要生成代码。
StaticExtension
通过StaticExtension可以将目标属性设置成指定静态属性或者字段的值。这个扩展标记往往带有一个命名资源属性或者字段的参数,该参数的形式为ClassName.MemberName。下面示例就是使用这个扩展标记获得了SystemColors类其中的一个属性值。
<TextBlock Background="{x:Static SystemColors.ActiveCaptionBrush}" Text="Foo"/>
请注意在实际过程中您往往不会像以上示例那样使用标记。由于它没有适当地与资源系统相结合,因此如果应用程序资源包含一个重载系统画刷的画刷,那这个示例将不会使用那个应用程序级的资源。同样的,也不会在属性发生变化时自动更新属性,也就是说StaticExtension只会获得一次属性值。其作用等价于以下代码。
TextBlock tb = new TextBlock();
tb.Background = SystemColors.ActiveCaptionBrush;
tb.Text = "Foo";
Code Behind(隐藏代码文件)
在前已经讲过,XAML是支持隐藏代码(Code Behind)这种理念,所谓隐藏代码理念,就是指界面和行为分开处理,其中XAML文件用来定义用户界面,而隐藏代码文件提供行为。
XAML是通过命名用partial类(分部类,前面已经讲过)来支持隐藏代码。通过partial类,一个类定义就可以在多个源文件中使用,而每一个单独的文件只包含一个partial类定义。在编译时,编译器会将这些结合起来组成一个完整的类定义。partial类的主要目的是允许生成的代码和手写的代码共用一个类而不必共用一个源文件。
如果您已经在XAML中定义了一个元素,并且想要在隐藏代码中进行使用,只需要设置其Name属性(类似于ASP.NET中的ID),如下例。
例1:已命名的元素
<Button Name="myButton">Click</Button>
通过以上标记,XAML编译器将会给类添加一个myButton的字段,并且会在初始化时进行设置以引用这个按钮,这样就可以在隐藏代码中编写代码来使用这个元素。如。
例2:从隐藏代码文件中使用已命名的元素
myButton.Background = Brushes.Green;
注意,并不是所有的类型都有一个Name属性,不过,你可以通过一个x:Name属性来代替隐藏代码为它们生成一个字段,并且将x:作为XAML命名空间的前缀,如。
例3:x:Name属性
<Button Content="Click">
<Button.Background>
<SolidColorBrush x:Name="bgBrush" Color="Yellow"/>
</Button.Background>
</Button>
在隐藏代码中使用该元素,在这里更改为元素的Color属性的值,如:
bgBrush.Color = Colors.Red;
那为什么会这样呢?事实上,你甚至可以在Name合法元素上使用x:Name样式,例如将例1中的Name替换成x:Name也不会改变它的行为。这是因为FrameworkElement将Name属性识别成x:Name属性的映射。这可以通过RuntimeNamePropertyAttribute来实现,如下例。
例4:将Name属性映射到x:Name
[RuntimeNamePropertyAttribute("Name"),...]
public class FrameworkElement : UIElement, ...
如果类型进行了这样的属性注释,那么在XAML中已命名的属性和x:Name之间就可以互相转换。但是如果类型中没有这种自定义属性的话,您就必须通过XAML中的x:Name属性来生成隐藏代码中的字段,即使目标类型有一个Name属性。
隐藏代码的其中一个主要任务就是制定应用程序响应用户输入的功能,因此,需要经常在XAML中把事件处理函数附加到元素上。首先的方法就是编写在隐藏代码初始化的过程中附加处理函数。如下示例。
<Button x:Name="myButton" Click="myButton_Click">Click</Button>
以上标记等价于在隐藏代码中附加处理函数,如下代码。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myButton.Click += myButton_Click;
}
}
尽管以上代码看起来与属性语法类似,不过XAML编译器将识别Click成员是一个事件,而不是一个属性。它会认为隐藏代码中提供了一个叫做myButton_Click的函数,并且它会将函数作为按钮Click的事件处理进行添加。但是,你必须确保函数有正确的签名,也就是说所有.NET事件都会有一个特定类型的函数签名。
至此,XAML基础的所有内容都总结完了,从下一篇开始将要总结的是Silverlight基础,欢迎大家与我讨论,祝大家新年快乐,龙年大吉。