XAML 基础
一旦你理解一些基本法则,XAML标准是十分直白的:
- 在XAML文档中,每个元素都映射到.NET类的一个实例。元素的名字恰好匹配类的名字。例如,元素<Button就是一个Button对象。
- 就像任何XML文档,你能在一元素内部嵌套另一个元素。如你所见,XAML使每个类可以灵活地处理这种情况。嵌套通常代表着包含关系—换句话说,如果你在一个Grid元素内部发现一个Button元素,在你的用户界面上,有一个网格,它的内部包含一个按钮。
- 你能通过特性设置每个类的属性。但是,有些情况下,一个特性不足以处理这个工作。在这种情况下,你将使用带有一个特殊语法的嵌套标签。
XAML名字空间
除了提供一个类的名字,XAML解析器也需要知道.NET类位于哪个名字空间。为得出你真正希望的类,XAML解析器检查应用于元素的XML名字空间。
xmlns特性是XML专用于声明名字空间的一个特性。
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
这个名字空间是WPF核心名字空间。包括所有的WPF类,包括用于建立界面的控件。它没有名字空间前缀,是整个文档的默认名字空间。换句话说,没有前缀的元素自动放在这个名字空间。
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
这个名字空间是XAML名字空间。它包括影响文档解释的各种实用特征。这个名字空间对应于前缀x,这意味着,你能依靠在元素名称前放上名字空间前缀来应用它。
后台代码类
通过在顶级元素设置Class特性,将XAML文档与后台代码类相关联:
<Window x:Class="WindowsApplication1.Window1"
x名字空间前缀放置Class特性到XAML名字空间。事实上,Class特性告诉XAML解析器用给定的名字生成一个新的类。那类派生自由XML元素命名的类。换句话说,这个例子创造了一个名为Window1的新类,它派生自Window类。
Window1类在编译时自动地生成。一个Window1.xaml文件链接着一个设计器自动生成的后台代码部分类,和一个程序员手写的代码部分类。
InitializeComponent()方法
当你创造Window1类的一个实例时,默认构造函数调用InitializeComponent()方法。此方法由编译器自动生成。
命名元素
为了后台代码能处理元素,需要为元素起一个名字:
<Grid x:Name="grid1"
解析器为Window1类添加一个名为grid1的字段:
private System.Windows.Controls.Grid grid1;
现在你可以使用grid1与Grid交互:
MessageBox.Show(String.Format("The grid is {0}x{1} units in size.", grid1.ActualWidth, grid1.ActualHeight));
对于继承自FrameworkElement的类来说,Name特性前的x前缀是可选的,去掉前面的x不会改变代码的含义。
XAML的属性和事件
简单属性和类型转换
XAML元素的特性值总是一个普通文本字符串,而对象的属性可以是任何.net类型。
WPF定义了类型转换器,在普通文本字符串与.net类型之间建立了一座桥梁。
对于类型转换器,XAML解析器依次执行如下:
- 解析器查找属性声明的TypeConverter特性。
- 解析器查找相应类型声明的TypeConverter特性。
解析器没有找到TypeConverter,则会生成一个错误。
复杂属性
一般一个类的属性对应元素的一个特性,使用的是属性-特性语法(property-attribute syntax)。有时类的属性非常复杂,则使用属性-元素语法(property-element syntax)。
用属性元素语法,你添加一个带有Parent.PropertyName形式名字的子元素。例如,Grid有一个Background属性接受一个Brush对象,如果刷子比较复杂,你需要添加一个命名为Grid.Background的子标签,如下所示:
<Grid Name="grid1"> <Grid.Background> ... </Grid.Background> ... </Grid>
使这工作的关键细节是元素名字中的点号(.)。这是属性与其它类型的嵌套内容的根本区别。
然后,如何设置属性元素呢?答案是在嵌套元素内部,你能添加另一个标签实例化一个指定的类。例如,用一个坡度刷子设置背景。为了定义你希望的坡度,需要创造一个LinearGradientBrush对象。
使用XAML的规则,依靠使用带有LinearGradientBrush名字的一个元素,你能创造LinearGradientBrush对象:
<Grid Name="grid1"> <Grid.Background> <LinearGradientBrush> </LinearGradientBrush> </Grid.Background> ... </Grid>
下一步,你也需要指定坡度颜色。依靠用GradientStop对象的一个集合填充LinearGradientBrush.GradientStops属性。再一次,GradientStops属性太复杂的不能单独用一个特性值设置。代替,你需要依赖属性元素语法:
<Grid Name="grid1"> <Grid.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Grid.Background> ... </Grid>
最后,你能用一系列GradientStop对象填充GradientStops集合。每个GradientStop对象有一个Offset和Color属性。你能依靠使用普通的属性特性语法提供这二个值:
<Grid Name="grid1"> <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标签集合都能被一套执行同样任务的代码语句替换。在此之前显示标签,用坡度填充背景,等价于下列代码:
var brush = new LinearGradientBrush(); var gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Red; brush.GradientStops.Add(gradientStop1); var gradientStop2 = new GradientStop(); gradientStop2.Offset = 0.5; gradientStop2.Color = Colors.Indigo; brush.GradientStops.Add(gradientStop2); var gradientStop3 = new GradientStop(); gradientStop3.Offset = 1; gradientStop3.Color = Colors.Violet; brush.GradientStops.Add(gradientStop3); grid1.Background = brush;
标记扩展
标记扩展能用在嵌套标签中或在XML特性中。当他们用在特性中时,他们总是被花括号{}括起来。例如,这里是标记扩展的用法,这允许你引用另一个类的一个静态属性:
<Button x:Name="cmdAnswer" Foreground="{x:Static SystemColors.ActiveCaptionBrush}" />
标记扩展的语法是 {MarkupExtensionClass Argument},在这个例子中,标记扩展是StaticExtension类。(按照惯例,当引用一个扩展类时,你能丢弃最后的单词Extension)。
标记扩展的基类是System.Windows.Markup.MarkupExtension。它只有一个ProvideValue方法,用于获得所期望的类实例。前面的例子中,XAML解析器首先构造了一个StaticExtension类(传入字符串"SystemColors.ActiveCaptionBrush"作为构造函数的参数),然后调用类的ProvideValue()方法,获得由SystemColors.ActiveCaption.Brush静态属性所返回的对象。
前面例子的XAML块,等价于下面的代码:
cmdAnswer.Foreground = SystemColors.ActiveCaptionBrush;
因为标记扩展映射到类,它们也能作为嵌套属性被使用。例如,前面例子的等价表示法:
<Button x:Name="cmdAnswer"> <Button.Foreground> <x:Static Member="SystemColors.ActiveCaptionBrush"></x:Static> </Button.Foreground> </Button>
依赖于标记扩展的复杂性和你希望设置的属性数目,这种语法有时更简单。
就像大多数标记扩展一样,StaticExtension需要在运行时被估值,因为只有那时你才能决定当前的系统颜色。一些标记扩展能在编译时被估值。这包括NullExtension。
附加属性
除了普通的属性,XAML也包含附加属性的概念—此属性可以应用于几个控件,但是被定义在另一个类中。在WPF,附加属性被频繁用于控制布局。
当你放置一个控件到一个容器内部时,它获得附加的特征,依赖于容器的类型。(例如,如果你放置一个文本框到一个网格内部,你需要能选择它所处的网格单元格)这些附加的细节使用附加属性被设置。
附加属性总是使用两部分名字:DefiningType.PropertyName。这个两部分命名的语法将普通属性和附加属性区分开来。
例如,附加属性允许每个控件将自己放置在网格的行中:
<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()时,解析器传递二参数:被修改的对象和指定的属性值。例如,当你设置文本框控件的Grid.Row属性,XAML解析器执行这代码:
Grid.SetRow(txtQuestion, 0);
这个代码暗示行数保存在Grid对象中,但实际上,行数保存在txtQuestion文本框对象中。
因为所有WPF控件都是DependencyObject,并且DependencyObject被设计为依赖属性的无限集合。
事实上,以上代码只是下面代码的快捷方式:
txtQuestion.SetValue(Grid.RowProperty, 0);
附加属性是WPF的一个核心成分。他们充当一个多功能的可扩展性系统。例如,依靠定义Row属性作为一个附加属性,它保证能用于任何控件。
嵌套元素
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元素是一个复杂的属性因为它包含一个点号。但是,解析器处理标签内部(三GradientStop元素)有点不同。在这种情况下,它知道GradientStops属性返回一个GradientStopCollection对象,并且知道GradientStopCollection实现IList接口。如此,它假定(十分正确)每个GradientStop应该被添加到集合,依靠使用IList.Add()方法:
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元素,它包含几个其它的控件:
<Grid Name="grid1"> ... <TextBox Name="txtQuestion" ... > ... </TextBox> <Button Name="cmdAnswer" ... > ... </Button> <TextBox Name="txtAnswer" ... > ... </TextBox> </Grid>
这些嵌套标签不是复杂属性因为他们没有包括点号。而且,Grid控件不是一个集合因为它没有实现IList或IDictionary。Grid真正支持的是ContentProperty特性,它是指可以接受任何嵌套内容的属性。从技术上,ContentProperty特性被应用于Panel类(Grid的基类),如同这样:
[ContentPropertyAttribute("Children")] public abstract class Panel
这是指任何嵌套元素应该被用于设置Children属性。依赖于内容属性是否是一个集合属性(在这种情况下,它实现IList或IDictionary接口),XAML解析器处理它的方式有所不同。因为Panel.Children属性返回一个UIElementCollection,并且UIElementCollection实现IList,解析器使用IList.Add()方法添加嵌套内容到网格。
换句话说,当XAML解析器遇见之前的标记,它创造每个嵌套元素的一个实例并且使用Grid.Children.Add()方法传递它到网格:
txtQuestion = new TextBox(); ... grid1.Children.Add(txtQuestion); cmdAnswer = new Button(); ... grid1.Children.Add(cmdAnswer); txtAnswer = new TextBox(); ... grid1.Children.Add(txtAnswer);
接下来发生什么完全取决于控制如何实现内容属性。Grid显示它在一个看不见的行和列组成的布局中持有的所有控件。
ContentProperty特性频繁地被使用在WPF。不仅它被用于容器控件(诸如Grid)和包含视觉项目集合的控件(诸如ListBox和TreeView),它也被用于包含单个内容的控件。例如,文本框和按钮控件能持有单个元素或文本,但是他们都使用一个内容属性处理嵌套内容,像这样:
<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属性接受任何元素。
因为Text和Content属性没有使用集合,你不能包括一个以上的内容。例如,如果你企图在一个按钮内部嵌套多个元素,XAML解析器将抛一个异常。如果你提供非文本内容(诸如一个长方形),解析器也抛出一个异常。
注意:ContentControl允许单个嵌套的元素,ItemsControl允许一个项集,Panel是布置一组控件的容器。ContentControl、ItemsControl和Panel基类都使用ContentProperty特性。他们的ContentProperty属性分别为:Panel.Children,TextBox.Text,Button.Content。
特殊字符和空白
见38页。
事件
特性除了映射到属性之外,特性也能被用于附加事件处理器。语法是EventName="EventHandlerMethodName"。
例如,为按钮附加点击事件处理器:
<Button ... Click="cmdAnswer_Click">
在许多情况下,你将在相同的元素上使用特性设置属性和附加事件处理器。WPF总是遵循相同的次序:首先它设置Name属性;然后它附加所有的事件处理器;最后它设置属性。这意味着当第一次设置属性时,就会触发相应的事件处理器。
使用其他名字空间
为使用一个没有定义在WPF名字空间中的类,你需要映射.NET名字空间到一个XML名字空间。
xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName"
三个斜体的信息解释如下:
Prefix:这是你希望使用的XML前缀,是指在你XAML标记中的名字空间。例如,XAML语言使用x前缀。
Namespace:这是全限定的.NET名字空间名字。
AssemblyName:类型在这个装配体中被声明,没有.dll扩展名。你的工程必须引用这个装配体。如果你希望使用你的工程装配体,留空。
例如,这里是你将如何访问在System名字空间的基本类型,并映射他们到前缀sys:
xmlns:sys="clr-namespace:System;assembly=mscorlib"
这里是你将如何访问当前工程、在MyProject名字空间的你已经声明的类型,并映射他们到前缀local:
xmlns:local="clr-namespace:MyNamespace"
现在,为了创造一个位于某名字空间的类实例,你使用名字空间前缀:
<local:MyObject ...></local:MyObject>
在XAML使用的类必须有无参数的构造函数。另外,你只能使用类中公开的属性。XAML不允许你设置公开的字段或调用方法。
如果你试图创造一个原始类型(诸如一个字符串,日期,或数字的类型),你可以提供你数据的字符串表示法作为你标签的内容。XAML解析器将随后使用类型转换器转换字符串到合适的对象。
<sys:DateTime>10/30/2013 4:30 PM</sys:DateTime>
DateTime类使用TypeConverter特性链接它自己到DateTimeConverter。DateTimeConverter认识这字符串是一个有效的DateTime对象并且转换它。当你使用这技术,你不能使用特性设置你对象的属性。
装载和编译XAML
仅代码
纯代码WPF,常用于根据数据库记录填充窗口。动态添加或替换窗口控件。
using System.Windows; using System.Windows.Controls; using System.Windows.Markup; public class Window1 : Window { private Button button1; public Window1() { InitializeComponent(); } private void InitializeComponent() { // Configure the form. this.Width = this.Height = 285; this.Left = this.Top = 100; this.Title = "Code-Only Window"; // Create a container to hold a button. var panel = new DockPanel(); // Create the button. button1 = new Button(); button1.Content = "Please click me."; button1.Margin = new Thickness(30); // Attach the event handler. button1.Click += button1_Click; // Place the button in the panel. IAddChild container = panel; container.AddChild(button1); //alternative panel.Children.Add(button1); // Place the panel in the form. container = this; container.AddChild(panel); //alternative this.AddChild(panel); //alternative this.Content = panel; } 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(); } }
代码和未编译的XAML
一个任意的文本文件如"TextFile1.txt"
<DockPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Button Name="button1" Margin="30">Please click me.</Button> </DockPanel>
创建窗口的代码:
using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Markup; public class Window2 : Window { private Button button1; public Window2(string xamlFile) { // Configure the form. this.Width = this.Height = 285; this.Left = this.Top = 100; this.Title = "Dynamically Loaded XAML"; // Get the XAML content from an external file. DependencyObject rootElement; using (FileStream fs = new FileStream(xamlFile, FileMode.Open)) { rootElement = (DependencyObject)XamlReader.Load(fs); } // Insert the markup into this window. this.Content = rootElement; // Find the control with the appropriate name. button1 = (Button)LogicalTreeHelper.FindLogicalNode(rootElement, "button1"); //alternative var frameworkElement = (FrameworkElement)rootElement; button1 = (Button)frameworkElement.FindName("button1"); // Wire up the event handler. button1.Click += button1_Click; } private void button1_Click(object sender, RoutedEventArgs e) { button1.Content = "Thank you."; } }
启动窗口:
public class Program : Application { [STAThread()] static void Main() { Program app = new Program(); System.Environment.CurrentDirectory = @"D:\Application Data\Visual Studio\Projects\WpfApplication2"; app.MainWindow = new Window2("TextFile1.txt"); app.MainWindow.ShowDialog(); } }
代码和编译的XAML
当你编译一个WPF应用时,Visual Studio使用两阶段编译处理。第一阶段是编译XAML文件到BAML。例如,如果你的工程包含一个文件Window1.xaml,编译器将创造一个临时文件Window1.baml并且把它放在obj\Debug子文件夹中。与此同时,一个部分类被创造,为了你的窗口,使用你的选择的语言。例如,如果你使用C#,编译器将在obj\Debug文件夹中创造一个文件Window1.g.cs。g代表生成(generated)。
public partial class Window1 : System.Windows.Window, System.Windows.Markup.IComponentConnector { // The control fields. internal System.Windows.Controls.TextBox txtQuestion; internal System.Windows.Controls.Button cmdAnswer; internal System.Windows.Controls.TextBox txtAnswer; private bool _contentLoaded; // Load the BAML. public void InitializeComponent() { if (_contentLoaded) { return; } _contentLoaded = true; System.Uri resourceLocater = new System.Uri("window1.baml", System.UriKind.RelativeOrAbsolute); System.Windows.Application.LoadComponent(this, resourceLocater); } // Hook up each control. void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) { switch (connectionId) { case 1: txtQuestion = ((System.Windows.Controls.TextBox)(target)); return; case 2: cmdAnswer = ((System.Windows.Controls.Button)(target)); cmdAnswer.Click += new System.Windows.RoutedEventHandler( cmdAnswer_Click); return; case 3: txtAnswer = ((System.Windows.Controls.TextBox)(target)); return; } this._contentLoaded = true; } }
仅XAML
没有创造任何代码,使用一个XAML文件,这被称为松XAML文件。松XAML文件能直接用ie浏览器打开。
为尝试一个松XAML页面,将正常的XAML文件做如下改动:
- 移除根元素的Class属性。
- 移除所有绑定事件处理器的属性。
- Window元素改为Page元素。