在WPF中,由于引入了XAML语言。因此在界面设计方面,一般使用XAML语言,而在业务逻辑上用C#这样的后台代码,XAML和后台代码既可以配合得丝丝入扣,又可以将界面设计和业务逻辑分离。这就好比两仪剑法和两仪刀法,一正一反。但均是从八卦中化出,再回归八卦,可以说是殊途同归。
4.1 从C#到XAML
XAML与C#不同,但是与HTML和XML一样是一种声明式语言。下面是一段完整的XAML代码示例:
<!--开始标签(Start Tag)--><Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200"><!--属性--><!--两者中间的内容--><Button.Content>Hello XAML</Button.Content></Button><!--结束标签(End Tag)-->
XAML由开始标签、结束标签及二者中间的内容组成,开始标签中包括一个命名空间(xmlns)和两个属性说明(Width与Height)。
下面是一个更为复杂的XAML文件的例子:
首先看显示效果:


<Window Title="XAML窗口" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="300" Height="200"><DockPanel><Button DockPanel.Dock="Left" Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/><Button DockPanel.Dock="Right"><Button.Background><LinearGradientBrush StartPoint="0,0" EndPoint="1,1"><GradientStop Color="Yellow" Offset="0.0"/><GradientStop Color="Red" Offset="0.25"/><GradientStop Color="Blue" Offset="0.75"/><GradientStop Color="LimeGreen" Offset="1.0"/></LinearGradientBrush></Button.Background>Hello XAML</Button></DockPanel></Window>
其中描述包含一个面板(DockPanel)的窗口(Window),在面板的左右侧各有一个按钮。需要注意的是,**窗口**不会像前面的按钮一样直接显示出来,**需要运行程序后才会弹出窗口**。
上面这段XAML代码相当于以下的C#代码:
public class MyWindow : Window{[STAThread]public static void Main(){MyWindow win = new MyWindow();win.Show();Application app = new Application();app.Run();}public MyWindow(){InitializeComponent();}private void InitializeComponent(){this.Title = "XAML窗口";this.Width = 300;this.Height = 200;DockPanel panel = new DockPanel();Button btn = new Button();btn.Content = "Hello XAML";btn.Background = new SolidColorBrush(Color.AliceBlue);btn.Margin = new Thickness(0, 5, 0, 10);DockPanel.SetDock(btn, Dock.Left);panel.Children.Add(btn);Button btn2 = new Button();btn2.Content = "Hello XAML";LinearGradientBrush brush = new LinearGradientBrush();brush.StartPoint = new Point(0, 0);brush.EndPoint = new Point(1, 1);brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0));brush.GradientStops.Add(new GradientStop(Colors.Red, 0.25));brush.GradientStops.Add(new GradientStop(Colors.Blue, 0.75));brush.GradientStops.Add(new GradientStop(Colors.LimeGreen, 1));btn2.Background = brush;DockPanel.SetDock(btn2, Dock.Right);panel.Children.Add(btn2);this.Content = panel;}}
不难发现,XAML代码在描述界面上比C#简洁得多。
XAML文件有两个重要组成部分:一是有完整开始和结束标签的要素,如 Window、DockPanel 和 Button等,称为“元素”(Element);二是依附于元素的要素,如 Width、Height 和 Background,称为“属性”(Attribute)。
在C#代码中创建一个对象并为其赋值等都会使用 using关键字声明其命名空间,在XAML中也是如此。在Window元素中的 xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation 就是声明的命名空间,该命名空间貌似一个网站。但是它不是,它仅仅是一个命名空间的标示而已。
在C#中通过 new 关键字创建一个 Button 对象,而在XAML中是通过一个开始标签和结束标签来实现的。还有种更简洁的写法是用一个反斜线“/”取代结束标签,称为“empty-element语法”,如下所示:
<Button DockPanel.Dock="Left"Background="AliceBlue" Margin="0 5 0 10" Content="Hello XAML"/>
新建完对象后,接下来的事情就是要为该对象设置属性。在C#里设置属性是一件非常简单的事情,double 类型的属性就给它赋予 double 类型的值,string类型的属性就给它赋予 string 类型的值等。但是**在XAML里**,无论你的属性是 double 还是 string,你**能给属性赋的值统统是字符串**。下表中列出了一些典型的属性赋值的例子。
| 类 | 属性 | C# | XAML |
|---|---|---|---|
| Window | Title | this.Title = “XAML窗口”; | Title=”XAML窗口” |
| Window | Width | this.Width = 300; | Width=”300” |
| Button | DockPanel.Dock | DockPanel.SetDock(btn, Dock.Left); | DockPanel.Dock=”Left” |
| Button | Background | btn.Background = new SolidColorBrush(Colors.AliceBlue); | Background=”AliceBlue” |
| LinearGradientBrush brush = new LinearGradientBrush(); brush.StartPoint = new Point(0, 0); brush.EndPoint = new Point(1, 1); brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0)); …… btn2.Background = brush; |
…… |
||
| Button | Content | btn.Content = “Hello XAML”; | Content=”Hello XAML” |
其中仅有 Window 的 Title 属性是字符串,在 C# 和在 XAML 中均直接将值设置成字符串。此外 Window 的 Width 类型也是一种基本型 double,在XAML中给该类型的属性赋值也非常简单,它们均属于“简单属性”。DockPanel.Dock 不是 Button 的属性,在XAML中将其值设置为 Left,表示该按钮在 DockPanel 的左侧。在C#中将其转换为一种方法,这种属性称为“附加属性”。
Button 的 Background 属性是一个画刷类型,第1个按钮的该属性在C#中创建了一个画刷对象,然后将该对象赋给属性。在XAML中直接用一个“AliceBlue”字符串为其赋值,使得更为简洁。这正是类型转换器起到的作用,本章的第4,5节类型转换器里一探究竟;第2个按钮的背景色是一个渐变画刷。Background 属性没有写在元素的标签中,而是如同一个子元素写在 Button 的开始和结束标签之间。前面设置 Background 属性的方法称为“Attribute语法”,后一种称为“Property-Element语法”。“Property-Element语法”得名是因为这个时候的 Background 属性设定更像是一个元素设定的方法,但是由于本质上还是一个属性,因此是Property-Element。
Button 的 Content 属性是一个特殊的属性,其类型为 Object。它不仅仅是一个属性,更确切的说是一个嵌套的元素。从理论上来说它可以设置为任何类型的对象。
此外在C#中还有很多情况是一个属性会去引用一个对象,或者将一个属性赋值成空。在XAML里该如何实现呢?(4.6节)事件的处理函数如何写,如何使用XAML构建一个应用程序,或者混合使用XAML和C#如何构建应用程序?(在4.7节分别使用XAML和C#构建应用程序——刀还是刀,剑依旧是剑和第4.8节使用XAML和C#构建应用程序——刀剑合璧里寻求答案)。
4.2 命名空间及其映射
4.2.1 WPF的命名空间
前面我们已经看到在 XAML 中声明一个命名空间是一个URL,即 http://schemas.microsoft.com/winfx/2006/xaml/presentation。
这是WPF命名空间的标识。XAML文件规定必须至少指定一个XML的命名空间。为了使该命名空间作用于整个文件,通常会将其放置在根元素中,如下所示:
<Page xmlns="http://schema.microsoft.com/winfx/2006/xaml/presentation"><Button Width="300" Height="200"><Button.Content>Hello XAML</Button.Content></Button></Page>
在 WPF 中只有以下4种元素可以作为根元素。
(1)Window:代表一个窗口。
(2)Page:类似一个网站的页面。
(3)Application:代表一个应用程序(参见第8章)。
(4)ResourceDictionary:代表一个逻辑资源的集合(参见第12章)。
上面XAML代码中的根元素不是Button吗?该文件在 IE 浏览器和 XamlPad 中都可以正常显示,这是为什么?
实际上这种松散的 XAML 文件会和一个 PresentationHost.exe 程序关联,只要双击该文件,就会执行 PresentationHost。该程序发现没有根元素,则建立一个 Page 类型的对象,然后将其设置为 Page 的 Content 属性。看上去根元素是 Button,但实际上还是 Page。任何继承自 FrameworkElement 的元素都可以看上去作为根元素,但是只有 Window 对象不可以,因为它不能作为某个元素的子元素。
我们知道WPF中不同的控件分属于不同的命名空间,如 Window 类型属于 System.Windows 命名空间,而 Button 属于 System.Windows.Controls 命名空间。如果在C#代码中用到这些控件,则必须连续使用 using 语句,直到包括所有用到的命名空间,如下所示:
using System.Windows;using System.Windows.Controls;using ......
而在WPF中一个URL标识就可以全部涵盖所有用到的控件的命名空间,原因是其在 PresentationFramework 和 PresentationCore 这样的程序集中使用 XmlnsDefinition 将 Xml 和 CLR 命名空间关联起来。下面是通过 Reflector 与和 System.Windows 及 System.Windows.Controls 相关联。XAML解析器会检查应用程序所加载的所有组件的 XmlnsDefinition 属性,以此确定对应的 CLR 命名空间。

4.2.2 XAML的命名空间
第二个常用的命名空间是 XAML 的命名空间,如果需要使用 XAML 专用的元素和属性(事实上一定会使用),那么必须声明该声明空间。习惯上 XAML 的声明空间被声明成 x 前缀——xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml",一般使用 VS生成的默认的XAML文件都会包含 WPF 和 XAML 命名空间的声明,如下所示:

其中 Window、Grid 和 Button 属于 WPF 命名空间中的元素,x:Class 属于 XAML 命名空间中的属性。由于 WPF 命名空间是默认空间,因此 Window、Grid 和 Button 没有声明前缀。

4.2.3 其他命名空间
1、使用系统类
除去前面两个命名空间,如果XAML文件要使用其他 .NET 对象或者应用程序或者其他程序集中的自定义对象,也要声明命名空间。如下所示:

如果希望明确地将 Button 的 Content 属性设置为 String 类型(尽管这样做是画蛇添足),由于 String类型属于程序集 mscorlib.dll 且其命名空间为 System,所以在声明后才能使用,如下所示:
注意通过声明xmlns : s="clr-namespace : System; assembly=mscorlib"关联 System 命名空间和前缀 s,引号内的字符串开始是 clr-namespace,用来指定命名空间。assembly 用来指定程序集名称。
2、使用自定义类
有时需要自定义类,然后在 XAML 文件中使用,其中有如下两种情况:
(1)使用本地程序的自定义类
例如,在本地程序中定义一个 Book 类,如下所示:
namespace mumu_customnamespace{public class Book{public Book() { }public string Name { get; set; }public double Price { get; set; }}}
如果需要在XAML文件中使用该类,必须做如下声明:
xmlns : local=”clr-namespace : mumu_customnamespace”
由于Book类属于本地应用程序,因此声明中忽略了设置 assembly,如下所示:
xmlns:local=”clr-namespace:mumu_customnamespace” Title=”Window1” Height=”300” Width=”300”>
但是运行程序,按钮上面只会显示该类的类型。显然这不是我们想要的效果,解决办法就是重载 ToString 函数,如下所示:
public override string ToString(){string str = Name + "售价为:" + Price + "元";return str;}
再次运行,结果如下:

自定义类必须有一个默认的无参数的构造函数,比如下面的 Book 类,程序可能运行时会因无法实例化该类而报异常:
public class Book
{
**public Book(string name, double price){ }**
……}
但是编译器有时也可能会自动地添加一个无参数的构造函数,因此这样的问题算是一类不容易发现的 Bug。我们**<font style="color:#F5222D;">在自定义类时最好显式地添加无参数的构造函数</font>**以避免此类 Bug。
(2)使用外部程序的自定义类
使用外部程序的自定义类需要设置 assembly,如新建一个类型为 Class Library,名为“mumu_customlib”的项目。然后将刚才的 Book 类添加到其中。

现在一个解决方案中有两个项目,其中 mumu_customnamespace 是一个本地应用程序;mumu_customlib 是一个外部程序集。首先在本地应用程序中添加对 mumu_customlib 的引用,然后在XAML文件中声明并使用 Book 类,如下代码所示:
xmlns:customlib=”clr-namespace:mumu_customlib; assembly=mumu_customlib”** Title=”Window1” Height=”300” Width=”300”>
4.3 简单属性和附加属性
4.3.1 简单属性
前面讨论的 Title 和 Width 属性均为简单属性,为其赋值时一定要将值放在双引号之内。XAML解析器会根据属性的类型执行隐式转换,将字符串转换为相应的 CLR 对象。
其中枚举类型的属性设置会略有不同,在C#中设置为一个枚举类型的值通常是“类型 .值”。而在XAML中则省略前面的“类型”。在如下代码中为 SolidColorBrush 的 Color 属性直接赋值,没有前面的类型 Colors。
C#
SolidColorBrush solidbrush = new SolidColorBrush( );
solidbrush.Color = Colors.AliceBlue;
XAML
在C#中还有一种枚举类型的值可以通过“或”运算符(|)组合而成,如下所示:
public enum CarOptions{SunRoof = 0x01;Spoiler = 0x02;FogLights = 0x04;TintedWindows = 0x08;}class FlagTest{static void Main(){CarOptions options = CarOptions.SunRoof | CarOptions.FogLights;}}
此类枚举值称为“flagwise枚举值”,不能组合的枚举值称为“nonflag枚举值”。在WPF中前者非常少见,下面代码中可能会用到该枚举值。Glyphs 对象可以用来描述文字,其 StyleSimulations 属性是一个 flagwise 枚举类型。在这里我们将字体设置为加粗和斜体,值中间用“,”取代了C#中的“|”。
<Glyphs
FontUri=”C:\WINDOWS\Fonts\TIMES.TTF”
FontRenderingEmSize=”100”
StyleSimulations=”BoldSimulation,ItalicSimulation”
UnicodeString=”Hello XAML!”
Fill=”Black”
OriginX=”100”
OriginY=”200”
/>

即使是该属性,WPF中也提供了一个 BoldItalicSimulation 枚举值,从而省略了枚举值之间的组合。
4.3.2 附加属性
附加属性是可以用于多个控件,但是在另外一个类中定义的属性。在WPF中该类属性常用在布局中,如前面例子中的 DockPanel.Dock=”Left”。附加属性的命名方式是“定义类型 .属性”,这样可以让XAML解析器将其与普通属性区分开。附加属性的设置可以使用 Attribute 和 Property-Element 语法,使用后者时类型必须是包含该属性的类型,如下代码所示:
<Button><!--这里是DockPanel,而不是Button--><DockPanel.Dock>Right</DockPanel.Dock>Hello XAML</Button>
4.4 Content属性
在前面的例子中可以看到 XAML 文件好像是一棵大的嵌套树,Window包含 StackPanel、DockPanel 和 Button,其中 Content属性起到了重要的作用。很多 WPF 类中都有这样一个特殊的属性,即 Content(内容)属性,我们在前面已经看到 Button 和 Content 属性放置的是一个字符串。
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"Width="300" Height="200"><Button.Content>Hello XAML</Button.Content></Button>
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"Width="300" Height="200" Content="Hello XAML"></Button>
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"Width="300" Height="200">Hello XAML</Button>
Content属性值可以放在 Button 元素中间的任何位置,如下面代码中的“Hello XAML”放置在 Button 元素的末尾处,同样也可以放置在标记1和2处。
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"><!--1 Hello XAML--><Button.Width>300</Button.Width><!--2 Hello XAML--><Button.Height>200</Button.Height>Hello XAML</Button>
但是 Content 不能分开放置,不允许在 Content 的值中插入其他标签。以下的写法是错误的:
例外情况是 TextBlock 的 Content 属性中可以放置一些加粗或者斜体标签,如下所示:
<TextBlock xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"><Italic>Hi everyOne,</Italic>This is <Bold>XAML</Bold></TextBlock>
Content属性不需要使用 Property-Element 语法即可直接成为 Button 元素的子类,**这是因为当用户定义一个类时使用 ContentProperty 来说明某个属性是 Content 属性**,这样WPF就知道该属性可以不需要使用 Property-Element 语法。如 Button 类,通过 Reflector 可以查看到其基类 ContentControl 的标识,如下所示:

同样可以为前面的 Book 类添加一个Text属性,并且将其指定为 Content 属性。然后在 ToString 函数中添加 Text 的内容,如下所示:
[ContentProperty("Text")]public class Book{public string Text{ get; set; }public override string ToString(){string str = Name + "售价为:" + Price + "元" + "\n" + Text;return str;}...}
在 XAML 文件中也可以按照内容属性语法来设置 Book 的 Text 属性值。如:
<Button FontSize="14"><customlib:Book Name="葵花宝典" Price="0.1">欲练此功 必先自宫</customlib:Book></Button>
WPF中到底有哪些类有 Content 属性,它们的 Content 属性的名字又是什么?

从上图中可以看出不仅仅是名称为 Content 属性才是真正的 Content属性,而 Panel 的 Children 及 MenuBase 的 Items 属性也是 Content 属性。这两种属性和 Content属性不同的是它们是一个集合属性,可以放置一个或多个对象。
4.5 类型转换器
4.5.1 功能
XAML解析器通过类型转换器跨越字符串值和非字符串值的鸿沟,在XAML中输入的字符串通过类型转换器将这些字符串转换为相应的 CLR 对象。这就是类型转化器所起到的作用,如下图所示:

所有的类型转换器都派生自 TypeConverter。TypeConverter 提供的4个重要的方法是 CanConvertTo、CanConvertFrom、ConvertTo 和 ConvertFrom。
ConvertFrom 方法将 XAML 中的**字符串转换为相应的 **CLR 对象;ConvertTo 方法将 **CLR 对象转换为相应的字符串**;CanConvertFrom 用来检查能否从字符串转换为相应的 CLR 对象;CanConvertTo 检查 CLR 对象能否转换为相应的字符串,如果可以,则返回 true,否则返回 false。
从 TypeConverter 派生的转换器类有 100 多个,这里不一一列举。比较常用的比如 BrushConverter 就可以实现从字符串转换成相应的画刷类型。但是 XAML 是如何知道何种属性使用什么转换器呢?XAML 解析器实际上通过两个步骤来查找类型转换器:
(1)检查属性声明查找 TypeConverter 特性,如窗口的 Width 和 Height 属性(派生自 FrameworkElement)前面都会声明一个类型转换器。下图是通过 Reflector 查看到的 FrameworkElement 源码片段:

(2)如果在属性声明中没有 TypeConverter 特性,XAML解析器会检查对应数据类型的类的声明。如按钮的 Background 属性类型是 Brush,并在 Brush类头部就声明了一个 BrushConverter 转换器,这样在设置 Background 属性时 XAML 解析器会自动应用该转换器。下图是通过 Reflector 查看到的 Brush源码片段:

4.5.2 自定义类型转换器
我们还是以自定义的类 Book,说明如何使 Book 的 Price 属性稍作改进能够支持人民币,还能支持美元。如:
<local:Book Name="葵花宝典" Price="0.1"/>
我们认为葵花宝典的价格还是 0.1 元,如果写成:
<local:Book Name="葵花宝典" Price="$0.1"/>
则认为葵花宝典的价格是 0.8 元(假定 1 美元 = 8 元)。
为 Price 定义一个新的类型为 MoneyType,它只是简单的封装了一个 double 类型的变量。同时提供了一个静态的 Parse 方法将一个字符串正确转换为 MoneyType 类型的对象,如下所示:
[TypeConverter(typeof(MoneyConverter))]public class MoneyType{private double _value;public MoneyType(){_value = 0;}public MoneyType(double value){_value = value;}public override string ToString(){return _value.ToString();}public static MoneyType Parse(string value){string str = (value as string).Trim();if(str[0] == '$'){string newprice = str.Remove(0, 1);double price = double.Parse(newprice);return new MoneyType(price * 8);}else{double price = double.Parse(str);return new MoneyType(price);}}}
在第一行中为这个类提供了一个 MoneyConverter 类型的转换器。下面是其实现。为了使这个类看起来比较完整,将实现其 4 个方法。事实上关键的是 ConvertFrom 方法,通过 MoneyType 的静态方法 Parse 将字符串转换为正确的 MoneyType 对象,如下代码所示:
public class MoneyConverter : TypeConverter{public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType){if(sourceType == typeof(string))return true;return base.CanConvertFrom(context, sourceType);}public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType){if(destinationType == typeof(string))return true;return base.CanConvertTo(context, destinationType);}public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value){if(value.GetType() != typeof(string))return base.ConvertFrom(context, culture, value);return MoneyType.Parse((string)value);}public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType){if(destinationType == typeof(string))return base.ConvertTo(context, culture, value, destinationType);return value.ToString();}}
在原来的XAML文件中 Price 的值前面加上一个“$”符号,**<font style="color:#E8323C;">同时,将Price的类型从 double改成 MoneyType</font>**,再次运行程序,结果如下:

4.6 标记扩展
现在绝大多数的属性 XAML 都可以工作得很好了,但是依旧会有几种情况 XAML 难以胜任。
(1)将一个属性赋值为 null。
(2)将一个属性赋值给一个静态变量,如将按钮的 Background 赋值为一个预定义画刷,参看如下代码(在C#中将Background属性设置为一个静态变量):
Button btn = new Burron();
btn.Content = “Hello XAML”;
btn.Background = SystemColors.ActiveCaptionBrush;
……
以上这几种情况,我们就需要使用标记扩展了。类型转换器悄无声息的实现了类型转换,而**标记扩展则是通过 XAML 的显式的、一致的语法调用实现**。标记扩展比类型转化器更为强大,和类型转换器一样,标记扩展也可以通过自定义来实现 XAML 的语义扩展。
在 XAML 中只要属性值被一对花括号{}括起,XAML 解析器就会认为这是一个标记扩展,而不是一个普通的字符串。前面的两种情况都可以用标记扩展的方法来解决,将一个按钮的 Background 属性赋值为空,表示这个按钮的背景颜色为透明色。{x:Null}是 XAML 中提供的一种标记扩展,表示一个空值,如下代码所示:
<Button Name="btn" Content="MyButton" Click="btn_Click" **Background="{x:Null}"**>
XAML 中提供了一个{x:Static}的标记扩展,可以引用一个类的静态变量,如下所示:
<Button Name="btn" Content="MyButton" Click="btn_Click"><Button.Background><x:Static Member="SystemColors.ActiveCaptionBrush"/></Button.Background></Button>
有时候,我们只想显示一个字符串,但是不巧字符串里就有一对花括号({}),如下代码所示,这个时候XAML解析器会固执地认为这是一个扩展标记。由于它又无法找到 HelloXAML 这样的标记扩展,所以无法编译通过。
<TextBlock Text="{HelloText}"/>
解决这个问题地方法是在该字符串前面添加一个空的花括号,以指示XAML 它只是一个普通的字符串,而不是一个扩展标记,如下代码所示:
<TextBlock Text="{}{HelloText}"/>
4.7 分别使用XAML和C#构建应用程序——刀还是刀,剑还是剑
4.7.1 XAML——反两仪刀法
XAML语言,我们称之为标记式(Markup)语言。它的这种层次结构天生地适合描述WPF界面。XAML既适合机器阅读,又适合人类阅读。
XAML还有一种其他代码语言(C#或者VB)难以比拟的优点,即甚至不需要编译,可以在 IE 或者在 XamlPad 中直接浏览。这种文件称为“松散XAML文件”。可以容易地将其从桌面应用移植到Web应用。
尽管我们一直在WPF范畴内讨论XAML,但是XAML与WPF不相互依赖,WPF完全可以只用代码语言来实现应用程序。XAML也可以用在以下方面:
(1)WPF XAML:本书所讨论的XAML
(2)XPS XAML:WPF XAML 的一部分,它定义了一个表示格式化的电子文档的XML。
(3)Silverlight XAML:Silverlight 是一个跨平台的浏览器插件,它是WPF的一个子集。可以使用XAML来描述Silverlight 的二维图形、动画,以及音频和视频以创建富互联网应用。
(4)WF XAML:描述 Window工作流基础内容的元素,可以参阅相关书籍。
XAML 毕竟是一种相对简单的声明式语言,不适用于应用程序的业务逻辑实现。下面代码为一个仅仅依靠 XAML 实现的应用程序,该项目由 App.xaml 和 Window1.xaml文件组成。
<Applicationxmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"StartupUri="Window1.xaml"></Application>
<Windowxmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"Title="AppByOnlyXAML" Height="200" Width="600"><StackPanel Width="500"><Button Content="MyButton"/><Ellipse Stroke="Red" Height="60" StrokeThickness="3"/></StackPanel></Window>
应用程序的入口是 App.xaml文件,XAML 文件的根元素之一,即 Application。该文件的 BuildAction 必须设置为 ApplicationDefinition(右击App.xaml文件,选择快捷菜单中的Properties选项,然后在打开的 Properties 对话框中设置)。Application 元素的 StartupUri 属性设置需要启动的窗口文件,在这里是 Window1.xaml文件,如下图所示:

Window1.xaml 文件的 Build Action 选项设置为 Page,该窗口中包含一个面板StackPanel。其中放置一个按钮和一个椭圆形,运行结果如下图所示:

如果单击按钮令椭圆的边界色由红色变成蓝色,则难以处理。XAML 毕竟是一种简单的声明式的语言,对于这样的应用程序业务逻辑显得力不从心,因此需要更为强大的C#语言——正两仪剑法。
4.7.2 C#——正两仪剑法
C#强大到任何XAML写的应用程序都可以轻易地通过C#实现,我们通过代码方式将这个 StackPanel 作为一个窗口的 Content 属性显示。如下代码所示,函数 InitializeComponent主要用来描述界面设置。虽然有的时候我们会抱怨 XAML 过于冗长,但是在描述界面方面,它确实比用代码的方式要简洁和清晰很多。
using System;using System.Windows;using System.Windows.Controls;using System.Windows.Shapes;using System.Windows.Media;namespace mumu_appbyonlycode{public class WindowByOnlyCode : Window{public WindowByOnlyCode(){InitializeComponent();}private Ellipse elip;public void InitializeCompoment(){this.Width = 600;this.Height = 200;this.Title = "AppBuOnlyCode";StackPanel panel = new StackPanel();panel.Width = 500;Button btn = new Button();btn.Content = "MyButton";panel.Children.Add(btn);elip = new Ellipse();elip.Stroke = new SolidColorBrush(Colors.Red);elip.Height = 60;elip.StrokeThickness = 3;panel.Children.Add(elip);this.Content = panel;}[STAThread]public static void Main(){Application app = new Application();app.Run(new WindowByOnlyCode());}}}
程序运行结果同上节,只是修改了窗口的标题,如下所示:

现在可以方便地实现上一节的业务逻辑,在 InitializeComponent 函数中为 Button 的 Click 事件注册事件处理器 btn_Click,然后在其中将椭圆的颜色修改成蓝色,如下代码所示:
public void InitializeComponent(){......btn.Click += new RoutedEventHandler(btn_Click);}void btn_Click(object sender, RoutedEventArgs e){elip.Stroke = new SolidColorBrush(Colors.Blue);}
4.8 使用XAML和C#构建应用程序——刀剑合璧
XAML用来描述界面,C#用来实现业务逻辑。
4.8.1 第一次刀剑合璧
一个最为直接的想法是编写一个程序,解析前面例子中XAML的字符串,通过反射机制最终将这些字符串变成一个对象。这样的想法虽然直接,但是会是一种失败的“合璧”,会呈几何倍数地增大工作量。如果WPF提供了这样的解析类,则情况将不同。
WPF中提供了这样的解析类,在System.Windowa.Markup命名空间中包含一个 XamlReader 类,它通过一个静态的 Load 方法将 XAML 字符串解析为一个对应的对象。但是该方法不能直接接收字符串作为参数,而且需要一个 XmlReader对象作为输入参数,可以通过 StringReader 作为中介将字符串转换为一个 XmlTextReader 对象(XmlTextReader 派生自 XmlReader)。我们按照这样的思路,在前面的例子中第一次混合使用 XAML和C#来构建应用程序,代码如下所示:
using System;using System.Windows;using System.Windows.Controls;using System.Windows.Shapes;using System.Windows.Media;using System.Windows.Markuo;using System.Xml;using System.IO;namespace mumu_appbyxamlandcode1{public class WindowByXAMLAndCode : Window{public WindowByXAMLAndCode(){InitializeComponent();}public void InitializeComponent(){this.Width = 600;this.Height = 200;this.Title = "AppByXAMLAndCode";string strXaml = "<StackPanel xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Width='500'>" +"<Button Content='MyButton'/>" + "<Ellipse Stroke='Red' Height='60' StrokeThickness='3'/>" + "</StackPanel>";StringReader strReader = new StringReader(strXaml);XmlTextReader xmlreader = new XmlTextReader(strReader);StackPanel obj = (StackPanel)XamlReader.Load(xmlreader);Content = obj;}[STAThread]public static void Main(){Application app = new Application();app.Run(new WindowByXAMLAndCode());}}}
由于涉及 StringReader、XmlTextReader 和 XamlReader 几个新类,因此需要增加一个 System.Xml.dll 的引用,同时需要增加 System.Windows.Markup、System.Xml 和 Syatem.IO 命名空间。除了改变窗口的标题,程序运行结果和前面一节相同。
当然也可以不从字符串,而直接从一个松散XAML文件解析对象,如将前面的字符串写成一个松散XAML文件(mumu_stackpanel.xaml)后添加到项目中。注意 mumu_stackpanel.xaml文件的 Build Action选项(生成操作)一定要设置为 Resource,
如下代码所示:
public void InitializeComponent(){this.Width = 600;this.Height = 200;this.Title = "AppByXAMLAndCode";Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");Stream stream = Application.GetResourceStream(uri).Stream;StackPanel obj = (StackPanel)XamlReader.Load(stream);Content = obj;}
如果为 Button 添加事件,则需要为 mumu_stackpanel.xaml 添加 Name 属性,如下代码所示:
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Width="500"><Button Name="btn" Content="MyButton"/><Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/></StackPanel>
在 InitializeComponent 函数中通过 FindName 方法找到按钮和椭圆对象,将椭圆对象保存在事先定义的成员变量中,然后为按钮对象注册 Click 事件处理函数。在该函数中通过保存的椭圆对象来改变边界颜色,如下代码所示:
public class WindowByXAMLAndCode : Window{private Ellipse elip;public void InitializeComponent(){this.Width = 600;this.Height = 200;this.Title = "AppByXAMLAndCode";Uri uri = new Uri("pack://application:,,,/mumu_stackpanel.xaml");Stream stream = Application.GetResourceStream(uri).Stream;StackPanel obj = (StackPanel)XamlReader.Load(stream);Content = obj;elip = obj.FindName("elip") as Ellipse;Button btn = obj.FindName("btn") as Button;btn.Click += new RoutedEventHandler(btn_Click);}void btn_Click(object sender, RoutedEventArgs e){if(elip != null){elip.Stroke = new SolidColorBrush(Colors.Blue);}}......}
4.8.2 完美的刀剑合璧
1、Markup + Code + Behind
WPF提供了完美的刀剑合璧,看同样的例子。WPF的解决方案提供了 4 个文件(App.xaml 和 App.xaml.cs,MainWindow.xaml 和 MainWindow.xaml.cs),两两成对,如下图所示:

App.xaml 为应用程序的入口;App.xaml.cs 是其后台代码,用于实现业务逻辑,二者通过 x:Class 关联。在 XAML 文件中 x:Class 指定的类名必须和对应的 .cs 文件中的类名一致,在 .cs文件中类的声明必须加上关键字 partial。
MainWindow.xaml 为应用程序的主窗口,其中描述该窗口的界面组成。而对应的 .cs文件实现的是**事件响应处理函数,二者之间的关联和前面类似,但是注意必须在 MainWindow 的构造函数中调用 InitializeComponent方法**。
同样需要为按钮添加 Click 事件处理函数,在 MainWindow.xaml 文件的按钮标签中加上 Click=”btn_Click”。该函数的实现放在 MainWindow.xaml.cs 文件中,这就是 WPF中典型的 Markup + Code + Behind的做法。
4个文件的内容分别如下所示:
<Application x:Class="mumu_appbyxamlandcode2.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"StartupUri="MainWindow.xaml"><Application.Resources></Application.Resources></Application>
namespace mumu_appbyxamlandcode2{/// <summary>/// Interaction logic for App.xaml/// </summary>public partial class App : Application{}}
<Window x:Class="mumu_appbyxamlandcode2.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"Title="AppByXAMLAndCode2" Height="200" Width="600"><StackPanel Width="500"><Button Name="btn" Content="MyButton" Click="btn_Click"/><Ellipse Name="elip" Stroke="Red" Height="60" StrokeThickness="3"/></StackPanel></Window>
namespace mumu_appbyxamlandcode2{/// <summary>/// Interaction logic for Window1.xaml/// </summary>public partial class MainWindow : Window{InitializeComponent();}private void btn_Click(object sender, RoutedEventArgs e){if(elip != null){elip.Stroke = new SolidColorBrush(Colors.Blue);}}}
2、工作原理
我们看看WPF是如何将XAML文件和代码文件关联起来的:工程目录下 obj 子目录中的 Release 或者 Debug 子目录(取决于编译状态是 Debug 还是 Release)中后缀名为 .g.cs 的两个文件是App 和 MainWindow类的另外一部分。g(generated)指这些文件是自动产生的,两个文件的内容代码如下所示:
namespace mumu_appbyxamlandcode2{/// <summary>/// App/// </summary>public partial class App : System.Window.Application{/// <summary>/// InitializeComponent/// </summary>[System.Diagnostics.DebuggerNonUserCodeAttribute()]public void InitializeComponent(){#line 4 "..\..\App.xaml"this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);#line default#line hidden}/// <summary>/// Application Entry Point./// </summary>[System.STAThreadAttribute()][System.Diagnostics.DebuggerNonUserCodeAttribute()]//此处才是真正的函数入口public static void Main(){mumu_appbyxamlandcode2.App app = new mumu_appbyxamlandcode2.App();app.InitializeComponent();app.Run();}}}

我们可以看到代码①处才是真正的函数入口,App类仍从 Main函数开始,如下所示:
MainWindow.g.cs

在代码②和代码③处会发现 btn 和 elip 的字段声明,XAML 的名称和 XAML 文件中按钮和椭圆的 Name 属性一致。 btn 和 elip 会在 Connect 方法中(⑥)获得该窗口的按钮和椭圆对象的实例,并实现事件处理函数的注册。Connect 方法是IComponentConnector 必须实现的一个接口方法,在MainWindow 的 InitializeComponent 函数中(⑤)会根据初始的 xaml文件(这里是 MainWindow.xaml),将其转换为当前的应用对象。该函数调用 LoadComponent后将一个 bool 类型的私有变量设置为 true,以保证在程序的生命周期内请求的资源只加载一次。加载的资源是一个BAML文件(这里是MainWindow.baml),它和生成的 .g.cs 文件放在同一个目录下。
BAML 文件是一个二进制形式的XAML文件,它会作为一个二进制资源嵌入到程序集中,可以通过Reflector 工具查到,如下图所示。该文件比原来的 XAML文件小得多,这样会显著提高程序运行时的性能。

4.8.3 还有一种方法——在XAML中嵌入代码
其实 XAML和C#还有一种混合使用的方法,就是在XAML中嵌入C#代码,如下所示:

嵌入的程序代码需要使用 x:Code 元素以及 x:Code 中的CDATA(Character data,字符数据)节,它实际是XML中的规定。即必须以“<![CDATA[”开头,以“]]>”结尾。如果中间出现“]]>”这样的字符则会出现问题,如代码 array1[array2[i]]>5。这样的情况虽然罕见,但是一定要小心。如果遇到,则需要在“>”号中多加一个空格。
这样的方式看起来很方便,但是实际上很不灵活。为了嵌入代码该文件必须要使用 x:Class 关键字。此外 x:Code 中也不支持队命名空间的引用,即不能使用 using 这样的关键字,一个“松散”XAML文件变得只能编译执行。
