XAML语法
1、XAML文档的树形结构
XAML使用标签来定义UI元素,每个标签对应.NET Framework类库中的一个控件类。通过设置标签的Attribute,不但可以对标签所对应控件对象的Property进行赋值,还可以做一些额外的事件(如声明名称空间、指定类名等)。
UI通过树形逻辑结构来描述UI.类似于XML。
2、XAML中为对象属性赋值的语法
XAML中为对象属性赋值共有两种语法:
- 使用字符串进行简单赋值
- 使用属性元素(Property Element)进行复杂赋值
2-1 使用标签的Attribute为对象属性赋值
前情纪要,一个标签的Attribute里有一部分与对象的Property互相对应,“<”Rectangle“>”标签的Fill这个Attribute就是这样——它与Rectangle类对象的Fill属性对应。在MSDN文档库里可以查到,Rectangle.Fill的类型是Brush。Brush是一个抽象类,凡是以Brush为基类的类都可作为Fill属性的值。Brush的派生类有很多:
- SolidColorBrush:单色画笔
- LinearGradientBrush:线性渐变画刷
- RadialGradientBrush:径向渐变画刷
- ImageBrush:位图画刷
- DrawingBrush:矢量图画刷
- VisualBrush:可视元素画刷
需要注意的是,通过这种Attribute=Value语法赋值时,由于XAML的语法限制,Value只可能是一个字符串值。这就引出了两个问题: - 如果一个类能使用XAML语言来进行声明,并允许它的Property与XAML标签的Attribute互相映射,那就需要为这些Property准备适当的转换机制。
- 由于Value是个字符串,所以其格式复杂程序有限,尽管可以在转换机制里包含一定的按格式解析字符串的功能以便转换成较复杂的目标对象,但这会让最终的XAML使用者头疼不已。因为他们不得不在没有编码辅助的情况下手写一个格式复杂的字符串以满足赋值要求。
第一个问题的解决方案是使用TypeConverter类的派生类,在派生类里重写TypeConverter的一些方法;第二个问题的解决方法就是使用属性元素(Property Element)。
2-2 使用TypeConverter类将XAML标签的Attribute与对象的Property进行映射
2-3 属性元素
在XAML中,非空标签均有自己的内容(content)。标签的内容指的是夹在起始标签和结束标签之间的一些子级标签,每个子级标签都是父级标签内容的一个元素(Element),简称为父级标签的一个元素。顾名思义,属性元素指的是某个标签的一个元素对应这个标签的一个属性,即以元素的形式来表达一个实例的属性。代码描述为:
<ClassName>
<ClassName.PropertyName>
<!--以对象形式为属性赋值-->
</ClassName.PropertyName>
</ClassName>
这样,在这个标签内部就可以适用对象(而不是局限于简单的字符串)进行赋值。
如果把上面的例子用属性标签式语法改写一下,XAML代码是这样:
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Rectangle x:Name="rectangle" Width="200" Height="120">
<Rectangle.Fill>
<SolidColorBrush Color="Blue"/>
</Rectangle.Fill>
</Rectangle>
</Grid>
效果与先前代码一致。所以,对于简单赋值而言,属性元素语法并没有什么优势,反而让代码看起来有点冗长。但是,遇到属性是复杂啊啊对象时,这种语法的优势就体现出来了。如使用线性渐变画刷来填充这个矩形。
Title="MainWindow" Height="450" Width="800">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center">
<Rectangle x:Name="rectangle" Width="200" Height="120">
<Rectangle.Fill>
<LinearGradientBrush>
<LinearGradientBrush.StartPoint>
<Point X="0" Y="0"/>
</LinearGradientBrush.StartPoint>
<LinearGradientBrush.EndPoint>
<Point X="1" Y="1"/>
</LinearGradientBrush.EndPoint>
<LinearGradientBrush.GradientStops>
<GradientStopCollection>
<GradientStop Offset="0.2" Color="LightBlue"/>
<GradientStop Offset="0.7" Color="Blue"/>
<GradientStop Offset="1.0" Color="DarkBlue"/>
</GradientStopCollection>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</Grid>
LinearGradientBrush的GradientStops属性是一个GradientStop对象的集合(GradientStopCollection),即一系列的矢量渐变填充点。在这些填充点之间,系统会自动进行插值运算、计算出过渡色彩。填充矢量的方向是StartPoint和EndPoint两个属性(类型为Point)的连线方向,矩形的左上角为(0,0),右下角为(1,1)。这段代码中,针对这三个属性都使用了属性标签式的赋值方法。
2-4 标记扩展(Markup Extensions)
当需要为对象的属性进行这些特殊类型赋值时就需要使用标记扩展了。
所谓的标记扩展,实际上是一种特殊的Attribute=Value语法,其特殊的地方在于Value字符串是由一对花括号及其括起来的内容组成,XAML编译器会对这样的内容做出解析、生成相应的对象。
<StackPanel Background="LightSlateGray">
<TextBox Text="{Binding ElementName=slider1,Path=Value,Mode=OneWay}" Margin="5"/>
<Slider x:Name="slider1" Margin="5"/>
</StackPanel>
其中,Text="{Binding ElementName=slider1,Path=Value,Mode=OneWay}"这句就是标记扩展了。
分析一下这句代码:
-
当编译器看到这句代码时就会把花括号里的内容解析成相应的对象。
-
对象的数据类型名是紧邻左花括号里的字符串
-
对象的属性由一串以逗号连接的子字符串负责初始化(注意,属性值不再加引号)。
尽管标记扩展的语法简洁方便,但并不是所有对象都能用标记扩展的语法来写,只有MarkupExtension类的派生类(直接或者间接均可)才能使用标记扩展语法来创建对象。MarkupExtension的直接派生类不多,它们是: -
System.Windows.ColorConvertedBitmapExtension
-
System.Windows.Data.BindingBase
-
System.Windows.Data.RelativeSource
-
System.Windows.DynamicResourceExtension
-
System.Windows.Markup.ArrayExtension
-
System.Windows.Markup.NullExtension
-
System.Windows.Markup.StaticExtension
-
System.Windows.Markup.TypeExtension
-
System.Windows.ResourceKey
-
System.Windows.StaticResourceExtension
-
System.Windows.TemplateBindingExtension
-
System.Windows.ThemeDictionaryExtension
最后,使用标记扩展时还需要注意以下几点: -
标记扩展是可以嵌套的,例如“Text={Binding Source={StaticResource myDateSource},Path=PersonName}”是正确的语法。
-
标记扩展具有一些简写语法,例如“{Binding Value, …}”与“{Binding Path = Value, …}”是等价的,“{StaticResource myString,…}”与“{StaticResource ResourceKey = myString, …}”是等价的。两种写法中,前者称为固定位置参数(Positional Parameter),后者称为具名参数(Named Parameters)。固定位置参数实际上就是标记扩展类构造器的参数,其位置由构造器参数列表决定。
-
标记扩展类的类名均以单词Extension为后缀,在XAML使用它们的时候Extension后缀可以省略不写,比如Text="{x:Static …}“与写Text=”{x:StaticExtension …}"是等价的。
3、事件处理器与代码后置
在.NET事件处理机制中,可以为对象的某一个事件指定一个能与该事件匹配的成员函数,当这个事件发生时,.NET允许时会去调用这个函数,即表示对这个事件的响应和处理。因此,我们把这个函数称为“事件处理器”(Event Handler)。WPF支持在XAML里为对象的事件指定事件处理器,方法是使用事件处理器的函数名为对应对象事件的Attribute进行赋值:
<ClassName EventName="EventHandlerName"/>
当我们为一个XAML标签的事件性Attribute进行赋值时,XAML编辑器会自动为我们生成相应的事件处理器。事件处理器是使用C#语言编写的函数。以“<”Button“>”标签为例,当为Click赋值时,VS会有提示,按下Enter键,VS会自动生成一个事件处理器,并把它的名字(函数名)赋值给Click。如下:
<Button x:Name="button1" Click="button1_Click"/>
我们知道,C#语言编写的代码应该用于处理程序的逻辑,需要让它与表示UI的XAML代码分开。这些C#函数会放在哪里呢?由于C#支持partial类,XAML标签又可以使用x:Class特征指定将由XAML代码解析生成的类与哪个类合并,因此,我们完全可以把用于实现程序逻辑的C#代码放在一个文件里,把用于描述程序UI的XAML代码放在另一个文件里,并且让事件性Attribute充当XAML与C#之间沟通的纽带——设计师用XAML为程序创建UI并且展现给客户;程序员用C#编写逻辑、从后台支持前面的UI——这种将逻辑代码与UI代码分离、隐藏在UI代码后面的形式叫作“代码后置”(Code-Behind)。
注:
之所以能实现代码后置功能,是因为.NET支持partial类并能解析XAML所生成的代码与x:Class所指定的类进行合并。有两点需要注意的是:
- 不只是事件处理器,一切用于实现程序逻辑的代码都要放在后置的C#文件中。
- 默认情况下,VS为每一个XAML文件生成的后置代码文件名为“XAML文件全名.cs”,比如XAML文件名为MyWindow.xaml,那么它的后置代码文件名为MyWindow.xaml.cs。这样做是为了方便管理文件,但并不是必须的,只要XAML解析器能找到 x:Class 所指定的类,无论你的文件叫什么名字都可以。
介绍一个有意思的标签——x:Code,使用它可以把本来应该呆在后置代码里的C#代码搬到XAML文件里来。x:Code的内容一定要使用XML语言的<![CDATA[…]>转义标签。
<x:Code>
<![CDATA[
private void button1_Click(object sender,RoutedEventArgs e)
{
MessageBox.Show("Bye! Code-Behind!");
}
]]>
</x:Code>
4、导入程序集和引用其中的名称空间
.NET的模块称为程序集(Assembly)。一般情况下,是引用VS创建的是解决方案(Solution)。一个解决方案就是一个完整的程序。解决方案中会包含若干个项目(Project),每个项目是可以独立编译的,它的编译结果是一个程序集。常见的程序集是以.exe为扩展名的可执行程序或者是以.dll为扩展名的动态链接库,大多数情况下,我们说“引用其他程序集”的时候,说的都是动态链接库。因为.NET编程接口(API)以类和类级别的单元为主(Win32API是以函数为主),所以我们又常把引用程序集说成是引用类库。
名称空间的作用是避免同名类的冲突。
注意:想在自己的程序里引用类库,需要分三步来做:
- 编写类库项目并编译得到.dll文件或者获得别人编译的.dll文件。
- 将类库项目或者.dll文件引用进自己的项目。
- 在C#和XAML中引用类库中的名称空间。
一旦将一个类库引用进程序,就可以引用其中的名称空间。假设我的类库程序集名为MyLibrary.dll,其中包含Common和Controls两个名称空间,而且已经把这个程序集引用进WPF项目,那么在XAML中引用这两个名称空间的语法是:
xmlns:映射名="clr-namespace:类库中名称空间的名字;assembly=类库文件名"
对于MyLibrary.dll里的两个名称空间,XAML中的引用会是:
xmlns:common="clr-namespace:Common;assembly=MyLibrary"
xmlns:controls="clr-namesapce:Controls;assembly=MyLibrary"
分析一下XAML引用名称空间的语法:
- xmlns是用于在XAML中声明名称空间的Attribute,它从XML语言继承而来,是XML Namespace的缩写。
- 冒号后的映射名是可选的,但由于可以不加映射名的默认名称空间已经被WPF的主要名称空间占用,所以所引用的名称空间都需要加上这个映射名。映射名可以根据喜好自由选择,但团队内部最好使用一致的命名。一个建议就是使用类库中名称空间的原名或者缩写。
- 引号中的字符串值确定了你要引用的是哪个类库以及类库中的哪个名称空间。XAML编辑器可以帮我们自动填充字符串的写法。
一旦我们将类库中的名称空间引用XAML文档,我们就可以使用这些名称空间里的类。语法格式是:
<映射名:类名>...</映射名:类名>
例如使用Common和Controls中的类,代码是这样的:
<common:MessagePanel x:Name="window1"/>
<controls:LedButton x:Name="button1"/>
XAML中引用名称空间的语法与C#不太一样。最大的差别就是XAML需要为被引用的名称空间添加一个映射名,用这个映射名来代表被引用的名称空间。其实C#也可以这样引用名称空间,只是不经常用。