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解析器依次执行如下:

  1. 解析器查找属性声明的TypeConverter特性。
  2. 解析器查找相应类型声明的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元素。