XAML中为对象属性赋值共有四种语法:
1)使用字符串直接赋值;
这个不用多解释,就是属性类型正好是字符串,直接赋值即可,如x:Name="rect"。
2)使用字符串简单赋值(后台进行字符串的类型转换);
注意这里
那它们有区别吗?当然有。只是后台自动为我们将这个字符串转换为了Brush类型,所以我们感觉是一样的。
那以上Fill="Black"语句是使用的哪个画刷呢?答案是SolidColorBrush。
也许你会说单色画刷太单调了,确实如此。 Brush是一个抽象类,它的派生类还有很多:
SolidColorBrush:单色画刷
LinearGradientBrush:线性渐变画刷
RadialGradientBrush:径向渐变画刷
ImageBrush:位图画刷
DrawingBrush:矢量图画刷
VisulaBrush:可视元素画刷
那我们是否可以换一个其他画刷呢?可以。但是有点麻烦。因为SolidColorBrush画刷是微软已为我们完成了后台类型转换了,是默认画刷,所以支持简单赋值。而其他画刷未实现后台类型转换,是不能简单赋值。
要实现后台类型转换需要使用微软提供的类型转换机制,下面就举例介绍一下这套机制。
大家要明白,之所以要使用后台类型转换的原因是XMAL的语法限制,属性赋值只能是字符串。
public class Student
{
public string Name{get; set;}
public Student Classmate{get;set;}
}
这个类有两个属性,一个是string类型,一个是Student类型。现在我们需要在XAML中声明它,如下:
我们希望以上写法,Student的实例的Classmate赋一个Student类型的值,并且Student.Name就是赋值的字符串值。
在后台中如下使用:
Student stu = (Student)this.FindResource("stu");
MessageBox.Show(stu.Classmate.Name);
结果如何呢?编译通过,但运行时抛出异常,stu.Classmate实例不存在,也就是说编译器无法将我们赋值的Classmate="Tim"字符串转换为Classmate实例。
这里我们需要使用TypeConvert和TypeConverterAttribute这两个类完成类型转换工作。
首先,我们从TypeConvert类派生出自己的类,并重写它的一个ConvertFrom方法。这个方法有一个参数名为Value,这个值就是我们在XAML里的"Tim"。
public classs StringToStudentTypeConverter:TypeConverter
{
public override object ConvertFrom(lTypeDescriptorContext context,CultureInfo culture,object value)
{
if ( vlaue is string )
{
Student stu = new Student();
stu.Name = value as string;
return stu;
}
return base.ConvertFrom(context,tulture,value);
}
}
有了这个类还不够,还要使用TypeConverterAttribute这个特征类把StringToStudentTypeConverter这个类关联到作为目标的Student类上。
[TypeConverterAttribute(typeof(StringToStudentTypeConverter))]
public class Student
{
public string Name{get; set;}
public Student Classmate{get;set;}
}
现在大功告成,再次编译运行,将获得弹出窗口显示正确结果。
3)使用属性元素(Property Element)进行复杂赋值。
除了使用后台类型转换机制实现简单赋值,还有一种通过前台复杂赋值,它使用属性元素的方式。
在XAML中非空标签均具有自己的内容。标签的内容指的是夹在起始标签和结束标签之间的一些子级标签,每个子级标签都是父级标签内容的一个元素。属性元素指的是某个标签的一个元素对应这个标签的一个属性,即以元素的形式表达一个实例的属性。如下:
这段代码与先前的效果一样。所以,对于简单赋值而言,属性元素语法并没有什么优势,反而代码冗长。但遇到属性是复杂对象时,这种语法的优势就体现出来了。如使用
线性渐变画刷来填充这个矩形。
这段代码中,针对属性都使用了属性标签式赋值方法,完成了渐变画刷的赋值。这段代码感觉有点冗长,可读性不强,我不打算逐行解释,建议使用Blend工具直接生成。
4)使用标记扩展(Markup Extensions)赋值
复杂赋值是为属性生成一个新对象,但有时候需要把同一个对象赋值给两个对象的属性,还有时候需要给对象的属性赋一个null值,WPF甚至允许将一个对象的属性值依赖在其他对象的某个属性上。当需要为对象的属性进行这些特殊类型赋值时,就需要使用标记扩展。
所谓标记扩展,实际上一种特殊的Attribute=value语法,其特殊的地方在于Value字符串是由一对花括号及其括起来的内容组成,XAML编译器会对这样的内容做出解析,生成相应的对象。
标记扩展的作用同类型转换器,都是将字符串转换为相应类型的对象。WPF/Silverlight内建的标记扩展都派生自MarkupExtension。其应用场景如下,当我们想设定一个属性的值为一些特定的静态属性值,但我们在编译时不知道这个值(如一个由用户的配置来决定的颜色),这时候就可以使用标记扩展。简单说标记扩展是一种用来设置属性值的类。
与类型转换器不同的是,标记扩展通过XAML显式的,一致的调用,因此这是更好的扩展XAML的方法。另外标记扩展也可以完成一些类型转换器所不能完成的功能。例如,通过自定义一个标记扩展可以实现使用一个简单的字符串将控件的背景色设置为渐变笔刷,而使用类型转换器是无法完成的。
a. 标记扩展的语法及组成
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 |
模板绑定 |
下面逐一分析这些标记扩展:
1. NullExtension
NullExtension提供设置属性为空值的方法。在部分情况下,不设置属性值与显式设置为null的区别很大,尤其是属性已经被设置为某值,这时候设置为空相当于清除之前的设置。
上面的例子中对Button的Background属性的设置就展示了NullExtension的使用,将Background设置为null就可以清除之前所设置的背景,这是一个很好的例子。
2. TypeExtension
TypeExtension将返回一个System.Type对象给标记扩展所服务的Attribute。其接受一个定位参数,表示类型的名称。XAML将通过TypeExtension把这个字符串表示的类型名转化为相应的类型,同时这个类型名也不需要提供其完整命名空间(.NET),默认的命名空间就是该XAML的主命名空间与x:命名空间。
前文给TargetType属性设置值的标记扩展就是TypeExtension的一个例子。
3. StaticExtension
StaticExtension在前文有所提及,其将对象的属性设置为特定的静态值。其接受一个参数,确定属性的来源,参数格式为ClassName.PropertyName。
前文示例中设置Button的Height的代码演示了StaticExtension的使用。
StaticExtension存在的问题在于当属性(Property)变化时不能自动修改属性(Attribute)的值。另外单独使用StaticExtension不能很好的结合系统设置与程序设置。在实际应用中往往将StaticExtension与StaticResource扩展结合使用。
4. 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。
5. DynamicResource
DynamicResource将属性(Property)值与资源值联系起来,其使用方式与StaticResource相似,但可以跟踪资源改变。
<TextBlock Name="myText" Background="{DynamicResource {x:Static SystemColors.ActiveCaptionBrushKey}}"/> |
等效的C#:
myText.SetResourceReference(TextBlock.BackgroundProperty, SystemColors.ActiveCaptionBrushKey); |
由代码可以看出,在DynamicResource中,将StaticResource中资源赋值的方式改为设置引用,这样资源的值的改变可以被跟踪。这样当系统资源改变时,控件的背景也会随之改变。
6. 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类型的资源数组,最后将资源设置给列表框。
7. Binding
Binding标记扩展用来进行数据绑定。示例代码:
<TextBlock Text="{Binding Foo}" x:Name="txt"/> |
这段代码在数据上下文将对象的Text属性绑定到Foo。
BindingOperations.SetBinding(txt, TextBlock.TextProperty, b); |
8. 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; |