目录
XAML(Extensible Apllication Markup Language 的简写)是用于实例化.NET对象的标记语言。尽管XAML是一种可用于不同领域的技术,但其主要作用是构造WPF用户界面。
2.1 理解XAML
要处理图形丰富的复杂应用程序,最有效的方式是将图形部分从底层代码中分离出来。这样一来,美工人员可独立地设计图形,而开发人员可以独立地编写代码。
2.1.1 WPF 之前的图形用户界面
使用传统的显示技术,从代码中分离出图形内容并不容易。对于Windows窗体应用程序而言,关键问题是创建的每个窗体完全都由C#代码定义的。在将空间拖动到设计视图上并配置控件时,Visual Studio 将在相应的窗体类代码中自动调整代码。但图形设计人员没有任何可以使用C#代码工具。相反,美工人员必须将他们的工作内容导出位图。然后可使用这些位图确定窗体、按钮以及其他控件的外观。对于简单的固定用户界面而言,这种方法效果不错,但在其他一些情况下会受到很大的限制。这种方法存在以下几个问题:
- 每个图形元素(背景和按钮)需要导出单独的位图。这限制了组合位图的能力和使用动态效果的能力,如反锯齿、透明和阴影效果。
- 相当多的用户界面逻辑都需要开发人员嵌入到代码中,包括按钮的大小、位置、鼠标悬停效果以及动画。图形设计人员无法控制其中的任何细节。
- 在不同的图形元素之间没有固有连接,所以最后经常会使用不匹配的图像集合。跟踪所有这些项会增加复杂性。
- 在调整图形大小时必然会损失质量。因此,一个基于位图的用户界面是依赖于分辨率的。这意味着它不能适应大显示器以及高分辨率显示设置,而这严重背离了WPF的设计初衷。
WPF通过XAML解决了该问题。当在Visual Studio中设计WPF应用程序时,当前设计的窗口不被转换未代码。相关,它被串行化到一系列XAML标签中。当运行应用程序时,这些标签用于生成构成用户界面的对象。
2.1.2 XMAL 变体
实际上属于XAML有多种含义。到目前位置,我们使用XAML表示整个XAML语言,它时一种基于通用XML语法、专门用于表示一颗.NET对象树的语言。XAML还包含如下几个子集:
- WPF XAML
包含描述WPF内容的元素,如矢量图形、控件以及文档
- XPS XAML
是WPF XAML的一部分,它为格式化的电子文档定义了一种XML表示方式
- Silverlight XAML
是一个用于Mircrosoft Silverlight应用程序的WPF XAML子集。Silverlight是一个跨平台的浏览器插件,通过它可创建具有二位图形、动画、音频和视频的富Web内容
- WF XAML
包括描述WF(Work Flow,工作流)内容元素
2.1.3 XAML编译
WPF的创建者知道,XAML不仅要能够解决设计协作问题,它还需要快速运行。尽管基于XML的格式可以很灵活并且很容易迁移到其他工具和平台,但他们未必是最有效的选择。XML的设计目标是具有逻辑性、易读而且简单,没有被压缩。
WPF使用BAML(Binary Application Markup Language)来客服这个缺点。BAML并非新事物,它实际上就是XAML的二进制表示。当在Visual Studio中编译WPF应用程序时,所有XAML文件都被转换为BAML,这些BAML然后作为资源被嵌入到最终的DLL或EXE程序集中。BAML时标记化的,这意味着较长的XAML被较短的标记替代。BAML不仅明显小一些,还对其进行优化,从而使它在运行时能够快速解析。
2.2 XAML基础
- XAML 文档中的每个元素都映射为.NET类的一个实例。元素名称也完全对应于类名。例如,元素<Button>指示创建Button对象。
- 与所有XML文档一样,可在一个元素中嵌套另一个元素。XAML让每个类灵活地决定如何处理嵌套。但嵌套通常时一种表示“包含”的方法——换句话说,如果在一个Grid元素中发现一个Button元素,那么永华界面可能包括一个在其内部包含一个Button元素的Grid元素。
- 可通过特性设置每个类的属性。但在某些情况下,特性不足以完成这项工作。对于此类情况,需要通过特殊的语法使用嵌套的标签
以下文档表示一个新的空白窗口。
<Window x:Class="WindowsApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1"
Height="300"
Width="400" >
<Grid>
</Grid>
</Window>
该文档仅含有两个元素——顶级的 Window 元素以及一个 Grid 元素,Window元素代表 整个窗口,在Grid元素中可以放置所有控件。尽管可使用任何顶级元素,但是WPF应用程序只使用以下几个元素作为顶级元素:
- Window元素
- Page元素(该元素和Window元素类似,但它用于可导航的应用程序)
- Application元素(该元素定义应用程序资源和启动设置)
与在所有XML文档中一样,在XAML文档中只能有一个顶级元素。在上例中,这意味着是要使用</Window>标签关闭了Window元素,文档就结束了。在后面不能再由任何内容。
查看Window元素的开始标签,将发现几个有趣的特性,包括一个类名和两个XML名称空间。还会发现三个属性:Title="Window1" Height="300" Width="300" 每个特性对应Window类的一个单独属性。总之,这告诉WPF创建标题为“Window1”的窗口,并使窗口额尺寸为400*300单位
2.2.1 XAML名称空间
显然,只提供类名是不够的。XAML解析器还需要知道类位于那个.NET名称空间。例如,在许多名称空间中可能都有Window类——Window类可能时指System.Windows.Window类,也可能时指位于第三方组建中的Window类,或您自己再应用程序中定义的Window类等。为了弄清除实际上希望使用哪个类,XAML解析器会检查应用于元素的XML空间。
下面时该机制的工作原理。以下定义了两个名称空间
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns 特性时XML中的一个特殊特性,它专门用来声明名称空间。这段标记声明了两个名称空间,在创建的所有WPF XAML文档中都会使用这两个名称空间:
- http://schemas.microsoft.com/winfx/2006/xaml/presentation
WPF核心名称空间。它包含了所有的WPF类,包括用来构建用户界面的控件。在该例中,该名称控件的声明没有使用名称控件前缀,所以它称为整个文档的默认名称空间。换句话说,除非另行指明,每个元素自动位于这个名称空间。
- http://schemas.microsoft.com/winfx/2006/xaml
XAML名称空间。它包含各种XAML实用特性,这些特性可影响文档的解释方式。该名称空间被映射为前缀x。这意味着可通过在元素名称前放置名称空间前缀x来使用该名称空间。
正如再前面看到的,XML名称空间的名称和任何特定的.NET名称空间都不匹配。XAML的创建者选择这种设计的原因由两个。按照约定,XML名称空间通常时URI。这写URI看起来像时再指明Web上的位置,但实际上不是。通过使用URI格式的名称空间,不通组织就基本不会无意中使用相同的名称空间创建不同的基于XML的语言。因为schemas.com域归Mircrosoft所有,只有Microsoft会在XML名称空间的名称中使用它。
另一个原因时XAML中使用XML的名称空间和.NET名称空间不是一一对应的,如果一一对应的话,会显著增加XAML文档的复杂程度。此处的问题在于,WPF包含了十几种名称空间(所有这些名称空间都以System.Windows开头)。如果每个.NET名称空间都由不同的XML名称空间。那就需要为使用每个空间制定确切的XML名称空间,这很快就会XAML文档变得回环不堪。所以,WPF创建人员选择了这种方法,将所有这些.NET名称空间组合到单个XML名称空间中。因为在不同的.NET名称空间中都一部分WPF类,并且所有这些类名称都不相同,所以这种设计时可行的。
名称空间信息是的XAML解析器可找到正确的类。例如,当查找Window和Grid元素时,首先查找默认名称空间,然后查找相应的.NET名称空间,直至找到System.Windows.Window类和System.Windows.Controls.Grid类。
2.2.2 代码隐藏类
可通过XAML构造用户界面,但为了使应用程序具有一定的功能,就需要用与连接包含应用程序代码的事件处理程序的方法。XAML通过使用 Class特性 简化了这个问题:
<Window x:Class="WindowsApplication1.Window1">
在XAML名称空间的 Class特性 之前放置了名称空间前缀 x,这意味着这是XAML语言中更通用的部分。实际上,Class特性 告诉XAML 解析器用 指定的名称 生成一个新类。该类继承自由XML元素命名的类。换句话说,该类创建了一个名为Window1的新类,该类继承自Window基类。
Window1类是编译时自动生成的。这正是令人感兴趣之处。您可以提供Window1的部分类,该部分类会与自动生成的那部分合并在一起。您提供的部分类正式包含事件处理程序代码的理想容器。
Visual Studio 会自动帮助您创建可以放置事件处理代码的部分类。例如,如果创建一个名为WindowsApplication1的应用程序,该应用程序包含为Window1的窗口,Visual Studio将首先提供基本的类框架:
namespace WindowsApplication1
{
public partial class Window1:Window
{
public Window1()
{
InitializeComponent();
}
}
}
在编译应用程序时,定义用户界面的XAML被转换为CLR类型声明,这写类型声明与代码隐藏类文件中的逻辑代码融合到一起,形成单一的单元。
1. InitializeComponent()方法
现在,Window1类尚不具有任何真正的功能。然而,它确实包含了一个非常重要的细节——默认构造函数,当创建的一个实例时,该构造函数调用InitializeComponent().
注意:
InitializeComponent()方法在WPF应用程序中扮演着重要角色。因此,永远不要删除窗口构造函数中的InitializeComponent()调用。同样,如果未窗口类中添加另一个构造函数,也要确保调用InitializeComponent()方法。
InitializeComponent()方法在源代码中不可见,因为它是在编译应用程序时自动生成的。本质上,InitializeComponent()方法的所有工作就是调用System.Windows.Application类的LoadComponent()方法。LoadComponent()方法从程序集中提取BAML,并用它来构建用户界面。当解析BAML时,它会创建每个控件对象,设置其属性你,并关联所有时间处理程序。
2. 命名元素
在代码隐藏类中,经常希望通过代码来操作控件。为了达到此目的,控件必须包含XAML Name特性。在上面的示例中,Grid控件没有包含Name特性,所以不能在代码隐藏类中对其进行操作。
<Grid x:Name="grid1">
</Grid>
可在XAML文档中手动执行这个修改,也可在Visual Studio设计器中选择该网格,并通过属性窗口设置其Name属性。无论使用哪种方法,Name特性都会告诉XAML解析器将这样一个字段添加到Window1类自动生成部分,同时可在隐藏类中对该网格进行操作。
private System.Windows.Controls.Grid grid1;
MessageBox.Show($"The grid is {grid1.ActualWidth}x{grid1.ActualHeight} units in size");
上面显示的Name属性时XAML语言的一部分,用于版主集成代码隐藏类。让人感到有些困惑的时,许多类定义了自己的Name属性。XAML解析器使用一种更聪明的方法来处理这一问题。可设置XAML Name属性(使用x:前缀),也可设置属于实际元素的Name属性(通过删除前缀)。对于这两种方式,接过都是相同的——指定的名称在自动生成的代码文件中使用,并用于设置Name属性。以下的标记等价于之前的标记:
<Grid Name="grid1">
</Grid>
只有当包含Name属性的类使用 RuntimeNameProperty 特性修饰之后,这才是可行的。RuntimeNameProperty 特性指示哪个属性的值将作为该类型的实例的名称(显然,通常是使用Name属性)。FrameworkElement类使用RuntimeNameProperty 特性进行修饰,所以上面的标记没有问题。
2.3 XAML 中的属性和事件
上图中显示的Eight Ball Answer窗口包含4个控件:一个Grid控件、两个TextBox控件和一个Button控件。安排和配置这些控件所需的标记比前面例子中的标记长的多。下面简要列出这些标记,其中一些细节用省略号代替,以便于描述整个结构:
<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="385.6"
Width="629.6" >
<Grid Name=grid1>
<Grid.RowDefinitions>
...
</Grid.RowDefinitions>
<Grid.Background>
...
</Grid.Background>
<TextBox Name="txtQuestion" ...>
...
</TextBox>
<Button Name="cmdAnswer" ...>
...
</Button>
<TextBox Name="txtAnswer" ...>
...
</TextBox>
</Grid>
</Window>
2.3.1 简单属性与类型转换器
元素的特性设置相对应对象的属性。例如,我们为上面的示例中的文本框设置了对齐、页边距和字体:
<TextBox Name="txtQuestion"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="10,10,13,10"
TextWrapping="Wrap"
FontFamily="Verdana"
FontSize="24"
Grid.Row="0" >
...
</TextBox>
为使上面的设置起作用,System.Windows.Controls.TextBox类必须提供以下属性:VerticalAlignment、HorizontallyAlignment、FontFami、FontSize等属性。
为了使这个系统能够工作,XAML解析器需要执行比表面上看起来更多的工作。XML特性中的值总是纯文本字符串。但对象的属性可以使任何.NET类型。在上面的示例中,有两个属性使枚举类型、一个字符串类型、一个整数类型,还有一个为Brush对象。
为了关联字符串值和非字符串属性,XAML解析器需要执行转换。由类型转换器执行转换,类型转换器使从.NET 1.0 起就已经引入的基础结构的一部分。
实际上,类型转换器在这个过程中扮演着重要角色——提供了实用的方法,这些方法可将特定的.NET数据类型转换为任何其他.NET类型,或将其他任何.NET类型转换未特定的数据类型,比如这种情况下的字符串类型。XAML解析器通过以下两个步骤来查找类型转换器:
- 检查属性声明,查找TypeConverter特性(如果提供了TypeConverter特性,该特性将指定哪个类可执行转换)。例如,当使用注入Foreground这样的属性时,.NET将检查Foreground属性的声明。
- 如果在属性声明中没由TypeConverter特性,XAML解析器将检查对应数据类型的类声明。例如,Foreground属性使用一个Brush对象。由于Brush类使用TypeConverter(typeof(BrushConverter))特性声明进行修饰,因此Brush类及其子类使用BrushConverter类型转换器。
- 如果属性声明或属性类的类型声明都没有与其关联的类型转换器,XAML解析器会发生错误。
注意
如果在类层次上设置一个类型转换器,该转换器将应用到所有使用这个类的属性上。另一方面,如果希望为某个特定属性微调类型转换方式,那么可以在属性声明中改用TypeConverter特性。
2.3.2 复杂属性
虽然类型转换器便于使用,但他们不能解决所有的实际问题。例如,有些属性时完备的对象,这些兑现更具有自己的一组属性。尽管创建供类型转换器使用的字符串表示形式是可能的,但使用这种方法时,语法可能十分复杂,并且容易出错。
幸运的是,XAML提供了另一种选择:属性元素语法。使用属性元素语法,可添加名称形式为Parent.PropertyName的子元素。例如,Grid控件有一个Background属性,该属性允许提供用于绘制控件背景区域的画刷。如果希望使用更加复杂的画刷——比单一固定颜色填充更高级的画刷——就需要添加名为Grid.Background的子标签。
<Window ... >
<Grid Name=grid1>
<Grid.Background>
...
</Grid.Background>
...
</Grid>
</Window>
真正起作用的重要细节是元素名称中的句点(.)。这个句点吧该属性和其他类型的嵌套内容区分开来。还有一个细节,即一旦识别出想要配hi的复杂属性,如何设置呢?这里由一个技巧:可在嵌套元素内部添加其他标签来实例化特定的类。在Eight Ball Answer示例中,用渐变色填充背景。
<Grid>
<Grid.Background>
<LinearGradientBrush>
...
</LinearGradientBrush>
</Grid.Background>
...
</Grid>
LinearGradientBrush类是WPF名称空间集合中的一部分,所以可继续使用默认的XML名称空间。但是,只是创建LinearGradieBrush 对象还不够——还需要为其指定渐变的颜色。通过使用GradientStop对象的集合填充LinearGradienBrush.GradientStops 属性可以完成这一工作。同样由于GradientStops属性太复杂,因此不能通过一个简单的特性设置该属性。需要改用属性元素语言
<Grid>
<Grid.Background>
<LinearGradientBrush>
<GradientStop Offset="0.00" Color="Red" />
<GradientStop Offset="0.50" Color="Indigo" />
<GradientStop Offset="1.00" Color="Violet" />
</LinearGradientBrush>
</Grid.Background>
...
</Grid>
2.3.3 标记扩展
有些情况下,不可能硬编码属性值。例如,可能希望将属性值设置为一个已经存在的对象,或者可能希望通过将一个属性绑定到另一个空间来动态地设置属性值。这两种情况都需要使用标记扩展——一种非常规的方法设置属性的专门语法。
标记扩展可用于嵌套标签或XML特性中。当用在特性中时,它们总是被花括号包围起来。例如,下面的标记演示了如何使用标记扩展,它允许引用另一个类中的静态属性:
<Button ... Foreground="{x:Static SystemColors.ActiveCaptionBrush}" >
<==>
<Button ... Foreground="{x:StaticExtention SystemColors.ActiveCaptionBrush}" >
标记扩展使用 {标记扩展类 参数}语法。在上面的示例中,标记扩展是StaticExtension类(根据约定,在引用扩展类时可以省略最后一个单词Extension)。x前缀只是在XAML名称空间中查找StationExtension类,还有一些标记扩展时WPF名称空间的一部分,它们不需要x前缀。
左右标记扩展都由集成自System.Windows.Markup.MarkupExtention 基类的类实现。MarkExtention 基类十分简单——它提供了一个简单的ProvideValue()方法来获取所期望的数值。换句话说,当XAML解析器遇到上述语句时,它将创建StaticExtension类的一个实例(传递字符串SystemColors.ActiveCaptionBrush作为构造函数的参数),然后调用ProvideValue()方法获取SystemColors.ActiveCaptionBrush静态属性返回的对象。最后使用检索的对象设置cmdAnswer按钮的Foreground属性。这段XAML的最终结果与下面的相同:
cmdAnsert.Foreground = SystemColors.ActiveCaptionBrush;
因为标记扩展映射为类,所以它们也可用作嵌套属性。
<Button ...>
<Button.Foreground>
<x:Static Member="SystemColors.ActiveCaptionBrush"/>
</Button.Foreground>
</Button>
2.3.4 附加属性
除普通属性外,XAML还包括附加属性的概念——附加属性是可用于多个控件但在另外一个类中定义的属性。在WPF中,附加属性常用于控件布局。
下面解释附加属性的工作原理。每个控件都有各自固有的属性(例如,文本框由其特定的字体、文本颜色和文本内容,这些是通过FontFamily、Foreground和Text属性指定的)。当在容器中放置控件时,根据容器的类型控件会获得额外的特征(例如,如果 哦在网格中放置一个文本框,就需要选择文本框放在网格控件中的哪个单元格中)。使用附加属性设置这些附加细节。
附加属性始终使用包含两个部分的命名形式:定义类型.属性名。这种包含两个部分的命名语法使XAML解析器能够区分开普通属性和附加属性。
在Eight Ball Answer实例中,通过附加属性在(不可见)网格的每一行中放置各个控件:
<TextBox Grid.Row="0" ...>
[Place question here.]
</TextBox>
<Button Grid.Row="1" ...>
Ask the Eight Ball
</Button>
<TextBox Grid.Row="2" ...>
[Answer will appear here.]
</TextBox>
附加属性根本不是真正的属性。XAML解析器采用以下形式调用静态方法:DefiningType.SetPropertyName()。例如,在上面的XAML代码段中,定义类型是Grid类,并且属性是Row,所以解析器调用Grid.SetRow()方法。
当调用SetPropertyName()方法是,解析器传递两个参数:被修改的对象以及指定的属性值。例如,当为TextBox控件设置Grid.Row属性时,XAML解析器执行以下代码:
Grid.SetRow(txtQuestion, 0);
这种方式(调用定义类型的一个静态方法)隐藏了实际发生的操作,使用起来非常方法。咋一看,这些代码好像将行号保存到Grid对象中。但行号实际上保存在应用它的对象中——对于上面的示例,就是TextBox对象。
这种技巧之所以能够奏效,是因为与其他所有WPF控件一样,TextBox控件继承自DependencyObject基类。DependencyObject类直在存储实际上没有限制的依赖项属性的集合。
实际上,Grid.SetRow()方法是和DependencyObject.SetValue()方法调用等价的简化操作,如下所示:
txtQuestion.SetValue(Grid.ROwProperty, 0);
2.3.5 嵌套元素
XAML让每个元素决定如何处理嵌套的元素。这种交互使用下面三中欧给机制中的一种进行中转,而且求职的顺序也是下面列出这三种机制的顺序:
- 如果父元素实现了IList接口,解析器将调用IList.Add()方法,并且为该方法传入子元素作为参数。
- 如果父元素实现了IDictionary接口,解析器将调用IDictionary.Add()方法,并且为该方法传递子元素作为参数。当使用字典集合时,还必须设置x:Key特性,以便于为每个条目指定键名。
- 如果父元素使用ContentProperty特性进行修饰,解析器将使用子元素设置对应的属性。
例如,前面的示例中的LinearGradientBrush画刷如何使用如下所示的语法,从而包含GradientStop对象集合:
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.00" Color="Red" />
<GradientStop Offset="0.50" Color="Indigo" />
<GradientStop Offset="1.00" Color="Violet" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
因为包含一个句点,所以XAML解析器知道 LinearGradientBrush.GradientStops 是复杂属性。但它需要以稍微不同的方式处理内部的标签。在这个示例中,解析器知道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>
注意
如果集合默认为null,那么需要包含用于指定集合类的标签,以便创建集合对象。如果有各异默认的集合实例而且只需要为它填充元素,那么可以忽略这一部分。
嵌套的内容并非总是指定为集合。例如,分析以下包含其他几个控件的Grid元素:
<Grid Name=grid1>
...
<TextBox Name="txtQuestion" ...>
...
</TextBox>
<Button Name="cmdAnswer" ...>
...
</Button>
<TextBox Name="txtAnswer" ...>
...
</TextBox>
</Grid>
这些嵌套的标签没有包含句点,因此并未对应于复杂属性。而且,Grid控件也不是集合,所以它也就没有实现IList或IDictionary接口。Grid控件支持ContentProperty特性,该特性指出应接收任何嵌套内容的属性。从技术角度看,ContentProperty 特性被应用于Panel类,而Grid类继承自Panel类,如下所示:
[ContentPropertyAttribute("Children")]
public abstact class Panel
这表明应使用任何嵌套元素来设置Children属性。XAML解析器根据是否是集合属性(集合属性实现了IList或IDictionary接口),采用不同方式处理内容属性。因为Panel.Children属性返回一个UIElementCollection对象,而且UIElementCollection类实现了IList接口,所以解析器使用IList.Add()方法将嵌套的内容添加到网格中。
换句话说,当XAML解析器遇到上面的标记时,会为每个嵌套的元素创建实例,并使用Grid.Children.Add()方法将创建的实例传递给Grid控件。
txtQuestion = new TextBox();
...
grid1.Children.Add(txtQuestion);
cmdAnswer = new Button();
...
grid1.Children.Add(cmdAnswer);
txtAnswer = new TextBox();
...
grid1.Children.Add(txtAnswer);
WPF中经常使用ContentProperty特性。该特性不仅用于容器控件(如Grid控件)和那些包含可视化条目集合的控件(如ListBox 和 TreeView控件),也用于包含单一内容的控件。例如,TextBox和Button控件只能包含一个元素或一段文本,但它们都使用内容属性来处理嵌套的内容,如下所示:
<TextBox Name="txtQuestion" ...>
[Place question here.]
</TextBox>
<Button Name="cmdAnswer" ...>
Ask the Eight Ball
</Button>
<TextBox Name="txtAnswer" ...>
[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解析器将抛出异常。如果提供非文本内容,解析器也会抛出异常。
2.3.6 特殊字符与空白
XAML受到XML规则的限制。例如,XML特别关注一些特殊字符,如&、< 和 >。如果试图使用这些字符设置元素的内容,将遇到麻烦。
特殊字符 | 字符实体 |
小于号 | < |
大于号 | > |
&符号 | & |
引号 | " |
特殊字符并非使用XAML的唯一障碍。另一个问题时空白的处理。默认情况下,XAML折叠所有空白,这意味着包含空格、Tab键以及硬回车的长字符串将被转换为单个空格。而且,如果在元素内容之前或之后添加空白,将完全忽略这个空格。在Eight Ball Answer示例中将看到这种情形。在按钮和两个文本框中的文本,使用硬回车字符从XAML标签中分离出来 ,并使用Tab字符使标记更加清晰易读。但多余的空格不会再显示再用户界面中。
优势这并不是所期望的结果。如果可能希望再按钮文本中包含一些列空格。这种情况下,需要 为元素使用xml:space="preserve"特性。
xml:space特性使XML标准的一部分,是一个要么包括全部、要么什么都不包括的设置。一旦使用了该设置,元素内的所有空白字符都被保留。
2.3.7 事件
特性也可用于关联事件处理程序。用与关联事件处理程序的语法为:事件名="事件处理程序方法名"。
<Button Click="cmdAnswer_Click" ...>
</Button>
private void cmdAnswer_Click(object sender, RoutedEventArgs e)
{
// Dramatic delay...
this.Cursor = Cursors.Wait;
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
AnswerGenerator generator = new AnswerGenerator();
txtAnswer.Text = generator.GetRandomAnswer(txtQuestion.Text);
this.Cursor = null;
}
2.4 使用其他名称控件中的类型
为使用未再WPF名称控件中定义的类,需要将.NET名称控件映射到XML名称控件。XAML有一种特殊的语法可用于完成这一项公国,该语法如下所示:
xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName"
通常,再XAML文档的根元素中,在紧随声明WPF和XAML名称控件的特性放置这个名称控件。
- Prefix
希望在XAML标记中用于只是名称控件的XML前缀。例如,XAML语言使用x前缀。
- Namespace
完全限定的.NET名称空间的名称
- AssemblyName
声明类型的程序集,没有dll扩展名。这个程序集必须在项目中引用。
xmlns:sys="clr-namespace:System;assembly=mscorlib"
理想情况是,希望在XAML中使用的每个类都有无参构造。如果具有无参构造函数,XAML解析器就可创建对应的对象,设置其属性,并关联所提供的任何事件处理程序。XAML不支持有参构造函数,而且WPF中的所有 所愿都 包含无参构造函数。此外,需要能够使用公共属性设置您所期望的所有细节。XAML不允许设置公共字段或调用方法。
如果想要使用的类没有无参构造函数,就有一些限制。如果试图创建简单的基本类型,可提供数据的字符串标识形式作为标签中的内容。XAML解析器接着将使用类型转换器将字符串转换为合适的对象。
<sys:DateTime>10/30/2010 4:30 PM</sys:DateTime>
因为DateTime类使用TypeConverter特性将自身关联到DateTimeConverter类,所以上面的标记可以奏效。DateTimeConverter类知道这个字符串是核发的DateTime对象,并对其进行转换。当使用该技术时,不能使用特性为对象设置任何属性。
如果想创建没有无参构造函数的类,但没有可用的类型转换器,那将是很不幸的。
2.5 加载和编译XAML
可使用三种不同的编码方式来创建WPF应用程序:
- 只使用代码
这是为Windows窗体应用程序使用的传统方法。它通过代码语句生成用户界面。
- 使用代码和未经编译的标记(XAML)
这种具体方式对于某些特殊情况是很有意义的,例如创建高度动态化的用户界面。这种方式在运行时使用System.Windows.Markup名称空间中的XamlReader类,从XAML文件中加载部分用户界面。
- 使用代码和编译过的标记(BAML)
对于WPF而言,这是一种更好的方式 。这种方式为每个窗口创建一个XAML模板,这个XAML模板被编译为BAML,并嵌入到最终的程序集中。编译过的BAML在运行时被提取出来,用于重新生成用户界面。
2.5.1 只使用代码
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
public class Windows1 : Window
{
private Button button1;
public Windows1()
{
InitializeCompent();
}
private void InitializeComponent()
{
this.Width = this.Height = 300;
this.Left = this.Top = 100;
this.Title = "Code-Only Window";
DockPanel panel = new DockPanel();
button1 = new Button();
button1.Content = "Please Click me";
button1.Margin = new Thickness(10);
button1.Click += button1_Click;
IAddChild container = panel;
container.AddChild(button1);
container = this;
container.AddChild(this);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
button1.Content = "Thank you.";
}
public class Program : Application
{
[STAThread]
static void Main()
{
Program app = new Program();
app.MainWindow = new Window1();
app.MainWindow.ShowDialog();
}
}
}
2.5.2 使用代码和未经编译的XAML
假设存在一个名为Window1.xaml的文件中使用下面的XAML内容。
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Button Name="button1" Margin="30">Please click me.</Button>
</DockPanel>
在运行时将其加载到一个已经存在的窗口:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.IO;
namespace NonCompiledXaml
{
public class Window1 : Window
{
private Button button1;
public Window1(string xamlFile)
{
InitializeComponent(xamlFile);
}
private void InitializeComponent(string xamlFile)
{
this.Width = this.Height = 285;
this.Left = this.Top = 100;
this.Title = "Dynamically Loaded XAML";
DependencyObject rootElement;
using (var fs = new FileStream(xamlFile, FileMode.Open))
{
rootElement = (DependencyObject)XamlReader.Load(fs);
}
this.Content = rootElement;
// Find the control with the appropriate name.
//button1 = (Button)LogicalTreeHelper.FindLogicalNode(rootElement, "button1");
FrameworkElement frameworkElement = (FrameworkElement)rootElement;
button1 = (Button)frameworkElement.FindName("button1");
// Wire up the event handler.
button1.Click += new RoutedEventHandler(button1_Click);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
button1.Content = "Thank you.";
}
}
}
注意
使用这种方法,确保松散的XAML文件不会像传统的XAML文件那样被编译到项目中。将文件添加到项目后,在Solution Explorer中选中该文件,然后使用Properties串口将Build Action设置未None,并将Copy to Output Directory设置未Copy Always。