一、基础
通过XAML可以构造用户界面,但是为了使应用程序具有一定的功能,就需要一个连接包含应用程序代码的事件处理程序的方法。XAML通过使用如下所示的Class特性使这一问题变得很简单
<Window x:Class="WindowsApplication1.Window1"
在XAML名称的Class特性之前放置了名称空间前缀x,意味着这是XAML语言中更通用的部分。实际上,Class特性告诉XAML解析器使用指定的名称生成一个新类。该类继承自由XML元素命名的类。换句话说,这个实例创建了一个名为Window1的新类,该类继承自Window基类。
Window1类是在编译时自动生成的。这正是问题的关键所在。您可以提供Window1的部分类(partial class关键字,允许在开发阶段把一个类分成多个独立的部分,并在编译过的程序集中把这些独立的部分融合到一起),该部分类会和自动生成的那部分合并在一起。您提供的部分类正是包含事件处理程序代码的理想容器。
Visual Studio会自动帮助您创建一个可以放置事件处理代码的部分类。例如,如果创建一个名为WindowApplication1的应用程序(实际上上面的XAML代码就是在这个应用中的),该应用程序包含一个名为Window1的窗口(就像上面的实例),Visual Studio会首先提供一个基本的类框架:
namespace WindowApplication1 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } } }
当编译应用程序时,定义用户界面的XAML被转换为CLR类型声明,这些类型声明和代码隐藏类文件(如这里的Window1.xaml.cs)中的逻辑代码融合到一起,形成单一的单元。
1. InitializeComponent()方法
现在,Window1只具有一个默认的构造函数。它调用了InitializeComponent()方法。这个方法再WPF中扮演着重要的角色。因此,永远要确保它在构造函数中被调用了。它在源代码中是不可见的,因为它是在编译应用程序时自动生成的。本质上,InitializeComponent()方法的所有工作就是调用System.Windows.Application类的LoadComponent()方法。LoadComponent()方法从程序集中提取BAML(编译过的XAML),并使用它构造用户界面。当解析BAML时,它会创建每个控件对象,设置其属性,并关联所有事件处理程序。
2. 命名元素
有时候为了在代码隐藏类文件(即对应的cs文件)中获得XAML中的一个控件,我们需要对其进行命名。可以用如下的方法添加XAML name特性
<Grid x:Name="grid1"> </Grid>
这样的Name属性是XAML语言的一部分。但是有些控件都有自己的Name属性(FrameworkElement基类就是一个例子,所有的WPF元素都继承自该类),不过在目前的XAML解析器看来,这两种是等价的,即
<Grid Name="grid1"> </Grid>
和上面的定义方法是一样的,不过前提是这个类要被RuntimeNameProperty修饰过后才可以。RuntimeNameProperty特性指示哪个属性的值将被作为该类型的实例的名称(显然,通常是使用Name属性)。FrameworkElement类使用RuntimeNameProperty特性进行了修饰,所以上面的标记是没有问题的。
二、XAML中的属性和事件
以下先给出一个应用程序Eight Ball的XAML框架(省去细节),然后参照此介绍XAML的语法
<Window x:Class="EightBall.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Eight Ball Answer" Height="328" Width="412" > <Grid> <Grid.RowDefinitions> ... </Grid.RowDefinitions> <Grid.Background> ... </Grid.Background> <TextBox ... Name="txtQuestion" ... > ... </TextBox> <Button Name="cmdAnswer" ... > ... </Button> <TextBox Name="txtAnswer" ... > ... </TextBox> </Grid> </Window>
1.简单属性与类型转换器
我们对第二个文本框设置了如下的属性:
<TextBox VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10,10,13,10" Name="txtAnswer" TextWrapping="Wrap" IsReadOnly="True" FontFamily="Verdana" FontSize="24" Foreground="Green" ...
在XAML中,这些属性值都是字符串,XMAL解析器则通过如下两个步骤来查找类型转换器并将它们解释为非字符串属性。
(1)检查属性声明,查找TypeConverter特性(如果提供了TypeConverter特性,该特性将指定哪个类可以执行转换)。例如,当使用Foreground属性时,.NET将检查Foreground属性的声明。
(2)如果在属性声明中没有TypeConverter特性,XAML解析器将检查对应数据类型的类声明。例如,Foreground属性使用一个Brush对象。因为Brush类使用TypeConverter(typeof(BrushConverter))特性声明进行了修饰,所以Brush类及其子类使用BrushConverter类型转换器。
如果以上方法都没有找到类型转换器,XAML解析器就会生成一个错误。
注意,XAML是基于XML的,所以它是大小写敏感的,所以我们只能使用<Button>而不能使用<button>,不过类型转换器通常是大小写不敏感的,所以Foreground="White"和Foreground="white"具有一样的效果。
2.复杂属性
有些属性是完备的对象,且具有自己的一系列属性,这时XAML提供了另一种定义方法:属性元素语法(property-element syntax)。用这个方法可以添加名称形式为Parent.PropertyName的子元素。例如,Grid控件有一个Background的属性,如果我们需要填充比单一固定颜色更高级的画刷,就需要用如下所示的方法:
<Grid> <Grid.Background> ...
</Grid.Background> ... </Grid>
真正起作用的重要细节是元素名称中的句点(.)。这个句点把该属性和其他类型的嵌套内容区分开来。
接下来就是要配置复杂属性了。可以通过在嵌套元素内部,添加其他标签来实例化特定的类的方法。譬如这里我们为了渐变填充,可以创建一个LinearGradientBrush对象,然后用GradientStop对象的集合填充LinearGradientBrush.GradientStops属性,指定其颜色,这里也使用到了属性元素语法。
<Grid> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background> ... </Grid>
当然,实际上可以为任何属性使用属性元素语法。
任何XAML标签集合都可以使用一系列执行相同任务的代码语句代替。上面的等价代码如下:
LinearGradientBrush brush = new LinearGradientBrush(); GradientStop gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Red; brush.GradientStops.Add(gradientStop1); GradientStop gradientStop2 = new GradientStop(); gradientStop2.Offset = 0.5; gradientStop2.Color = Colors.Indigo; brush.GradientStops.Add(gradientStop2); GradientStop gradientStop3 = new GradientStop(); gradientStop3.Offset = 1; gradientStop3.Color = Colors.Violet; brush.GradientStops.Add(gradientStop3); grid1.Background = brush;
3.标记扩展
有时候希望可以将属性值设置为一个已经存在的值,或者可能希望通过将一个属性绑定到另一个控件来动态地设置属性值。这时就需要使用标记扩展——一种以非常规的方式设置属性的专门语法。
标记扩展可用于嵌套标签或XML特性中(后者更为常见)。当用于XML特性中时,它们总是被花括号{}包围起来。例如,下面的标记演示了如何使用StaticExtension标记扩展,它允许引用另一个类中的静态属性:
<Button ... Foreground="{x:Static SystemColors.ActiveCaptionBrush}" ...>
标记扩展使用{标记扩展类 参数}语法。在上面的例子中,标记扩展是StaticExtension类(根据约定,在引用一个扩展类时可以省略最后一个单词Extension)。x的前缀指示一个XAML名称空间中查找StaticExtension类。还有一些标记扩展是WPF名称空间的一部分,它们不需要x前缀。
所有标记扩展都由继承自System.Windows.Markup.MarkupExtension基类的类实现。MarkupExtension基类非常简单——它提供了一个简单的ProvideValue()方法,该方法获取所期望的数值。换句话说,当XAML解析器遇到上面的语句时,它将创建一个StaticExtension类的实例(传递字符串SystemColors.ActiveCaptionBrush作为构造函数的参数),然后调用ProvideValue()方法获取SystemColors.ActiveCaption.Brush静态属性检索的对象。最后使用检索的对象设置cmdAnswer按钮的Foreground属性。
这段XAML的最终结果与下面的相同
cmdAnswer.Foreground = SystemColors.ActiveCaptionBrush;
因为标记扩展映射为类,所以它们也可以被用作嵌套属性,与上一节中学过的一样。例如,也可以像下面这样使用标记扩展
<Button ...> <Button.Foreground> <x:Static Member="SystemColors.ActiveCaptionBrush"></x:Static> </Button.Foreground> </Button>
根据标记扩展的复杂程度,以及想要设置的属性的数量,这种语法有时候更简单。
和大多数标记扩展一样,StaticExtension需要在运行时赋值,因为只有在运行时才能确定当前系统颜色。一些标记扩展可以在编译时评估。这些扩展包括NullExtension(该扩展代表一个null值)和TypeExtension(该扩展构造一个.NET类型的对象)。在本书中,将会看到许多实用标记扩展的例子,特别是在使用资源和数据绑定时。
4.附加属性
附加属性是可以用于多个控件但在另一个类中定义的属性。在WPF中,该属性经常用于控件布局。譬如用Grid布局的时候,其内嵌的空间有Grid.Row="0"的语句,这类属性就是附加属性。附加属性不是真正的属性,它们实际上被转换为方法调用:DefiningType.SetPropertyName。譬如上面的例子就是Grid.SetRow(textboxcontrol, 0)方法。这个方法之所以奏效,是因为WPF控件有共同的基类DependencyObject类。实际上,它和textboxcontrol.SetValue(Grid.Rowproperty, 0)是等价的,这个方法是DependencyObject里的方法。
5.嵌套元素
XAML文档被排列成一棵巨大的嵌套的元素树。XAML让每个元素决定如何处理嵌套的元素。这种交互使用下面的三种机制中的一种进行中转,并且求值的顺序也是下面列出这三种机制的顺序:
- 如果父元素实现了IList接口,解析器就会调用IList.Add()方法,并且为该方法传入子元素作为参数。
- 如果父元素实现了IDictionary接口,解析器就会调用IDictionary.Add()方法,并且为该方法传递子元素作为参数。当使用字典集合时,还必须设置x:Key特性以便为每个条目指定一个键名。
- 如果父元素使用ContentProperty特性进行了修饰,则解析器使用子元素设置对应的属性。
譬如上面提到的如下例子:
<LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush>
这里GradientStops属性返回一个GradientStopCollection对象,并且GradientStopCollection类实现了IList接口。因此,解析器使用IList.Add()方法将每个GradientStop对象添加到集合中。
GradientStop gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Red; IList list = brush.GradientStops; list.add(gradientStop1);
有些属性支持更多类型的集合。在这种情况下,需要添加一个标签来指定集合类,如下所示:
<LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush>
嵌套的内容并不总是指定为一个集合,比如一个Grid里面可以有TextBox,Button等,所以也就无法用IList或者IDictionary。Grid控件支持ContentProperty特性,该特性指出应当接收所有嵌套内容的属性。从技术角度讲,ContentProperty特性被应用于Panel类,而Grid类继承自Panel类。
[ContentPropertyAttribute("Children")] public abstract class Panel
这表明应该使用任何嵌套元素来设置Children属性。XAML解析器根据是否是一个集合属性(集合属性实现了IList接口或IDictionary接口),以不同的方式处理内容属性。因为Panel.Children属性返回一个UIElementCollection对象,并且因为UIElementCollection类实现IList接口,所以解析器使用IList.Add()方法将嵌套的内容添加到网格中。
换句话说,当XAML解析器遇到上面的标记时,会为每个嵌套的元素创建一个实例,并使用Grid.Children.Add()方法将创建的实例传递给Grid控件。下一步具体如何进行操作,完全取决于实现内容属性的方式。Grid控件在它不可见的行和列布局中显示它所含的所有控件。
ContentProperty特性在WPF中经常使用。不仅用于容器控件(如Grid控件)和那些包含可视化条目集合的控件(如ListBox控件和TreeView控件),而且也用于包含单一内容的控件。例如,TextBox控件和Button控件只能包含一个元素或一段文本,但是它们都使用内容属性来处理嵌套的内容,如下所示:
<TextBox ... > [Place question here.] </TextBox> <Button ...> Ask the Eight Ball </Button> <TextBox ...> [Answer will appear here.] </TextBox>
TextBox类使用ContentProperty特性标识了TextBox.Text属性。Button类使用ContentProperty特性标识了Button.Content属性。XAML解析器使用提供的文本来设置这些属性。
TextBox.Text属性只接受字符串。但是,Button.Content属性可以使用更多有趣的内容,Content属性可以接受任何元素。
<Button Name="cmdAnswer" ... > <Rectangle Fill="Blue" Height="10" Width="100" /> </Button>
因为Text属性和Content属性没有使用集合,所以只能包含一个内容。例如,如果试图在一个按钮中嵌套多个元素,XAML解析器将抛出一个异常。如果提供一个非文本内容(如一个Rectangle对象),同样会抛出异常。作为经验法则,所有继承自ContentControl类的控件只允许包含单一的嵌套元素。所有继承自ItemsControl类的控件都允许包含一个条目集合,该集合映射为控件的某些部分(例如条目列表或节点树)。所有继承自Panel类的控件都是用于组织多组控件的容器。ContentControl、ItemsControl和Panel基类都使用ContentProperty特性。
6.空白
默认时XAML折叠所有空白,如果想保留的话,需要加如下xml:space="preserve"特性:
<TextBox xml:space="preserve" ...> [There is a lot of space " "] </TextBox>