首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

2.8  编译:将XAML与过程式代码混合使用

WPF允许用任何一种.NET语言完全以过程式代码编写应用程序。另外,一些简单的应用程序可以完全写在XAML中,这多亏了在第9章中提到的数据绑定特性,以及在下一章中即将介绍的触发器,还要感谢一个事实——那就是松散XAML页面可以在IE浏览器中呈现。尽管如此,大多数WPF应用程序是XAML与过程式代码的混合体。本节将介绍两种XAML和代码混合的方式,然后了解一下XAML语言命名空间中的所有关键字,它们将帮助我们控制XAML和代码的交互。

2.8.1  在运行时加载和解析XAML

WPF的运行时XAML解析器公开为两个类,它们都位于System.Windows.Markup命名空间中:XamlReader和XamlWriter,而且它们的API已经再简单不过了。XamlReader包含了一些对静态Load方法的重载,而XamlWriter包含了一些对静态Save方法的重载。因此,用任何一种.NET语言写的程序都可以在运行时依赖XAML,而不用程序员付出太多努力。

1.XamlReader

XamlReader.Load方法的设置将解析XAML,创建合适的.NET对象,然后返回一个根元素的实例。因此,如果在当前目录下有一个XAML文件叫作MyWindow.xaml,它包含了一个Window对象(将在第7章中深入讲解)作为根结点,那么可以使用下面的代码来加载和获得Window对象:

获得根元素,该元素是一个Window对象

 

这个情况下,Load是与FileStream(位于System.IO命名空间中)一起被调用的。在Load返回之后,整个XAML文件的对象层级将在内存中被实例化,因此就不再需要XAML文件了。在前面的代码中,退出using代码块之后FileStream将被立即关闭。由于可向XamlReader传入一个任意的Stream(或者使用另一个重载来传入System.Xml.XmlReader对象),故有许多可选择的方式来获得XAML的内容。

既然现在已经有一个根元素的实例存在,就可以利用适当的内容属性或者集合属性来获得子元素。下面的代码假设Window有一个类行为StackPanel的子元素,StackPanel的第5个子对象是一个OK Button:

提示     XamlReader也定义了LoadAsync实例方法用于异步加载和解析XAML内容。例如,在加载大文件或者网络文件时,可以使用LoadAsync保持用户界面处于响应状态。而CancelAsync方法和LoadCompleted事件将协助以上方法,其中CancelAsync方法是用于停止处理的,LoadCompleted事件可以让我们知道处理何时完成。

通过(硬编码知识!)遍历子元素获取OK按钮

 

获得根元素,该元素是一个Window对象

 

有了这个Button的引用,就可以做任何想做的事:设置额外的属性(或许会使用一些难以用XAML表达的逻辑),添加事件处理程序,或者执行一些无法用XAML完成的动作,例如调用方法。

当然,使用硬编码的索引和其他关于用户界面结构假设的代码并不能让人满意,因为一旦稍微修改XAML它就无法工作了。相反,可以写一些代码来更通用地处理元素,并找到那个内容为“OK”字符串的Button元素,但是对于如此简单的任务来说,这样做就有些得不偿失了。另外,如果想让Button包含图形内容,该如何在多个按钮中识别出它呢?

幸运的是,XAML支持元素命名,这样就可以从过程式代码中找到这些元素并放心地使用它们了。

2.命名XAML元素

XAML语言命名空间有一个Name关键字,它是用来给元素命名的。如果是一个嵌入到窗口中的简单的OK Button,Name关键字可以这样使用:

有了以上代码后,就可以更改前面的C#代码了,可以使用Window的FindName方法来(递归地)搜索它的子元素,并返回想要的元素的实例。

获得根元素,该元素是一个Window对象

 

通过按钮的名称获得OK按钮

 

FindName并不仅仅在Window类中存在,在FrameworkElement、FrameworkContentElement及许多重要的WPF类的基类中也有FindName的定义,这些类可以在本书的封三看到。

不用x:Name也能命名元素

x:Name语法可以用于命名元素,但一些类也可以定义它们自己的属性来作为元素名称(通过加上System.Windows.Markup.RuntimeNamePropertyAttribute特性来实现)。例如,FrameworkElement和FrameworkContentElement有一个Name属性,因此它们的声明中加入了RuntimeNameProperty(“Name”)语句。这意味着在这样的元素中,可以只用一个字符串设置Name属性,而不需要使用x:Name语法。可以使用其中任何一种机制,但是不能同时使用它们。有两种方式设置名称会让人有些犯迷糊,但是因为这些类有Name属性,在过程式代码中使用会很方便,如果没有这样特殊处理一下,你能够同时在XAML中设置x:Name和Name,就会更加让人费解!

2.8.2  编译XAML

对于动态皮肤场景(将在第10章中讲到)来说,在运行时加载和解析XAML是有意义的,对于那些没有支持XAML编译的.NET语言也是有意义的。但大多数WPF项目会通过MSBuild和Visual Studio完成XAML编译。XAML编译包括三项事情:将一个XAML文件转换为一种特殊的二进制格式,将转换好的内容作为二进制资源嵌入到正在被创建的程序集中,然后执行链接操作,将XAML和过程式代码自动连接起来。在写本书的时候,C#和Visual Basic是最好的两种能够为XAML编译提供支持的语言。

如果你不在乎将XAML文件和过程式代码融合,那么只需要把它添加到Visual Studio的WPF项目中来,并用界面中的Build动作来完成编译即可。(第7章将讲解如何使用一个应用程序上下文中的内容。)但是如果要编译一个XAML文件并将它与过程式代码混合,第一步要做的就是为XAML文件的根元素指定一个子类,可以用XAML语言命名空间中的Class关键字来完成,例如:

让任何一种.NET语言都支持已编译的XAML

如果你想让某种.NET语言使用XAML编译,必须满足两个基本要求:有一个对应的CodeDom提供程序(provider)和一个MSBuild目标文件(target file)。另外,对于partial类的语言支持也是有用的,但不是必需的。

在一个独立的源文件中(但是在同一个项目中),可以定义一个子类,并添加任何想添加的成员:

一定要调用,这样才能加载XAML定义的内容!

 

通常我们把这样的文件叫作代码隐藏文件。如果你引用XAML中的任何一个事件处理程序(通过事件特性,如Button的Click特性),这里就是我们定义这些事件处理程序的地方。

类定义中的partial关键字很重要,因为类的实现是分布在多个文件中的。如果你使用的.NET语言无法支持部分类(如C++/CLI和J#),XAML文件就必须在根元素中定义一个Subclass关键字,如下所示:

改完后,这个XAML文件就完整定义了一个Subclass(本例中是MyWindow2),但是在代码隐藏文件中要把这个类(MyWindow)作为MyWindow2的基类。这样,我们依靠继承模拟了把代码分散在两个文件中的能力。

当在Visual Studio中创建一个基于WPF的C#或者Visual Basic项目,或者当使用“Add New Item…”来添加某个WPF项目时,Visual Studio会自动创建一个XAML文件,并把x:Class作为根元素,同时创建一个具有部分类定义的代码隐藏源文件,最后把两者连接起来,这样代码构建(build)才能顺利进行。

如果你是一个MSBuild的用户,并且想通过理解项目文件中的内容来使用代码隐藏,那么可以用一个简单的文本编辑器(如NotePad)打开本书源代码中包含的任何一个C#项目文件。但是一个项目的相关部分通常如下所示:

对于这样一个项目来说,在处理MyWindow.xaml时构建系统生成了几个项,它们是:

l    一个BAML文件(MyWindows.baml),作为默认的二进制资源它会被嵌入到程序集中。

l    C#源文件(MyWindow.g.cs),它会被编入程序集,就像其他源代码一样。

1.BAML

BAML是Binary Application Markup Language的缩写,意思是二进制应用程序标记语言,它其实是被解析、标记化(tokenized),最后转换为二进制形式的XAML。虽然大块的XAML代码可以被表示为过程式代码,但XAML到BAML的编译过程不会生成过程源代码。因此,BAML不像MSIL(Microsoft intermediate language,微软中间语言),它是一个压缩的声明格式,要比加载和解析普通的XAML文件快,且文件比普通XAML文件要小。BAML仅仅是XAML编译过程的详细实现,没有任何直接公开的方法,因此在未来它可能会被一些其他的东西所取代。不管怎样,了解它的存在是很有趣的。

2.生成的源代码

提示     x:Class只能在要编译的XAML文件中使用。但是有时在没有x:Class的情况下,编译XAML文件也是没有问题的。这其实意味着没有对应的代码隐藏文件,因此你不能使用任何需要过程式代码才能实现的特性。因此,在没有x:Class标签的情况下,添加一个XAML文件到Visual Studio项目中,是很方便的一种部署已编译XAML并提高性能的方式,而不用创建代码隐藏文件。

如果你使用x:Class的话,一些过程式代码确实是在XAML编译过程中生成的,但是这些过程式代码仅仅是“粘合代码(glue code)”,类似于在运行时加载和解析松散XAML文件所要写的代码,如那些后缀为.g.cs(或.g.vb)的文件,这里的g表示generated(生成)。

每个生成的源文件中包含了一个由根对象元素中的x:Class指定的类的部分类定义。XAML文件中的每个已命名的元素在该部分类中都有一个成员(默认是私有的),这些成员的名称就是元素名称。其中还有一个InitialzeComponent方法用于完成一大堆烦人的工作,包括加载嵌入BAML资源、向成员赋予适当的实例(这些实例是在XAML中定义的)、绑定所有的事件处理程序(如果事件处理程序已在XAML文件中指定的话)。

其实曾经有过CAML……

早期预发布的WPF版本有编译XAML为BAML或MSIL的能力。MSIL输出曾经叫作CAML,是Compiled Application Markup Language的缩写,译为已编译应用程序标记语言。这么做是为了能够有机会优化文件大小(针对BAML)或速度(针对CAML)。但是WPF团队决定不使用这两个独立的实现(做的事情本质上是一样的)来增加WPF基础代码的负担。之所以BAML能够战胜CAML,是因为它有如下几个优点:它比起MSIL执行程序要安全;它更加小巧(这可以让Web方案中的下载更少);它是在编译后本地化的。并且,人们已经通过理论证明,使用CAML并不比BAML要快多少。

因为生成的源文件中的粘合代码是你在代码隐藏文件中定义的类的一部分(且因为BAML是作为资源嵌入的),你通常不需要知道有BAML存在,也不需要处理它的加载和解析。而只要写一些代码来引用已命名的元素就可以了,就像引用其他类成员一样,然后构建系统把这些东西捆绑在一起。你唯一要记住的是,在代码隐藏类构造函数中调用InitializeComponent。

注意     不要忘记在代码隐藏类的构造函数中调用InitializeComponent!

如果你忘记了,那么根元素将不会包含你在XAML中定义的任何内容(因为对应的BAML没有被加载),任何表示已命名对象元素的成员都将变成null。

XAML中的过程式代码!

除了代码隐藏(有点像ASP.NET中的东西)之外,XAML实际上还支持“代码嵌入(code inside)”。可以用XAMl语言命名空间中的Code关键字实现,如下所示:

 

当上面的XAML文件编译后,x:Code元素中的内容将被放到部分类的.g.cs文件。注意:过程语言并不是在XAML文件中指定的,它是由包含该文件的项目决定的。

把代码<![CDATA[…]]>嵌入不是必须的,但是它至少避免了使用&lt作为;的替换符号以及&amp作为&的替换符号。这是因为虽然其他都是被当作XML来处理的,但XML解析器会忽略CDATA节。(这样做的代价是在代码中必须避免使用]]>,这是因为该符号会终止CDATA节!)

当然,没有一个合理的理由让我们使用“代码嵌入”特性来“玷污”你的XAML文件。代码嵌入特性除了让UI和逻辑变得更混乱以外,不受松散XAML页的支持,Visual Studio也不支持对它做语法着色。

 BAML可以反编译为XAML吗?

可以,因为无论怎么声明,任何一个公共的.NET类实例都可以被序列化为XAML。第一个步骤是获得一个实例,这个实例是用来作为根对象的。如果你还没有这个对象,可以调用静态的System.Windows.Application.LoadComponent方法,如下所示:

与之前的代码(用FileStream来加载.xaml文件)不同,这里使用的是LoadComponent,由URI(Uniform Resource Identifier,统一资源识别器)指定的名称并不要求物理上存在一个独立的.xaml文件。当指定了一个合适的URI(根据MSBuild的约定,应该是原来的XAML源文件的名字)之后,LoadComponent可以自动获得作为资源嵌入的BAML。实际上,Visual Studio自动生成的InitializeComponent方法就是调用Application.LoadComponent来加载嵌入的BAML,虽然它使用的是另一个重载。第8章将进一步详细讲解通过URI获得嵌入式资源的机制。

获得根元素的实例之后,可以使用System.Windows.Markup.XamlWriter类来获得根元素(以及它的任何一个子元素)的XAML表示。XamlWriter包含了5个静态Save方法的重载,是最简单的返回一个适当的XAML字符串的方法,通过传入一个对象实例来实现。例如:

听起来BAML似乎有些问题,因为它很容易被“啪的一声打开”,但它与其他运行在本地的或者本地显示UI的软件其实没有任何区别。(例如,你可以很轻易地打开一个网站的HTML、JavaScript和CSS文件。)

2.8.3  XAML关键字

XAML语言的命名空间(http://schemas.microsoft.com/winfx/2006/xaml)定义了一批XAML编译器或解析器必须特殊处理的关键字。它们主要控制元素如何被提供给过程式代码,但是即使没有过程式代码,有一些关键字还是有用的。你已经看到其中的一些了(如Key、Name、Class、Subclass和Code),而表2-1列出了所有的关键字。因为它们经常以x作为前缀出现在XAML和文档中,所以约定俗成地在代码清单中使用了x前缀。

由W3C定义的特殊特性

除了XAML语言的命名空间中的关键字以外,XAML也支持两个特殊的XAML特性,它们是由W3C(World Wide Web Consortium)组织定义的:

xml:space用于控制空白字符的解析,而xml:lang用于声明文档语言和文化。xml前缀是被隐式映射到标准的XML命名空间http://www.w3.org/XML/1998/namespace中的。

表2-2中是XAML语言命名空间中的一些其他项,但这些项可能会与关键字混淆,其实仅仅是标记扩展(如位于System.Windows.Markup命名空间中真正的.NET类)。。注意,表中忽略每个类的后缀Extension,因为通常使用时是不带后缀的。

表2-1  XAML语言命名空间中的关键字,采用习惯性的x作为命名空间的前缀

关 键 字

何处有效

含义/描述

x:Class

根元素的特性

为根元素定义一个派生自元素类型的类,可以在前面加上.NET命名空间作为前缀(可选)

x:ClassModifier

根元素的特性,必须与x:Class一起使用

定义由x:Class指定的类的可见性(该类默认是可见的)。该特性值必须根据使用的过程语言指定(如,C#中的public或internal)

x:Code

XAML中任何位置的元素,但是必须与x:Class一起使用

嵌入过程式代码,会被插入由x:Class指定的类中

x:FieldModifier

非根元素上的特性,但必须与x:Name(或者等效关键字)一起使用

定义生成的元素(默认是内部元素)字段的可见性,与x:ClassModifier一样,该值必须根据过程语言来指定。(如C#中的public、private等)

x:Key

父元素实现了IDictionary的元素的特性

当被添加到父元素的字典里时,请为该项指定键名

x:Name

非根元素上的特性,但必须与x:Class一起使用

为给元素生成的字段选择一个名称,这样它就可以在过程式代码中被引用

x:Shared

Resource-Dictionary对象中的元素特性,但只有在XAML编译后才可使用

可以被设置为false来避免在多个地方共享同资源实例,在第8章中有所讲解

x:Subclass

根元素的特性,必须与x:Class一起使用

为保存XAML内容的x:Class类指定一个子类,可以用.NET命名空间作为可选前缀(用于那些没有提供部分类支持的语言)

x:TypeArguments

根元素的特性,必须与x:Class一起使用

使根类成为泛型(如List<T>)且带指定的范型参数实例(如List<Int32>或List<String>),可以设置一个用逗号分割的泛型参数代码清单,如果某类型不在默认的命名空间里,需要加上XML命名空间前缀

x:Uid

元素的特性

为元素添加一个本地化ID,详见第8章

x:XData

用于某个IXmlSerializable类型属性的值的元素

对XAML解析器透明的任一个XML数据岛,详见第9章

表2-2  XAML语言命名空间中的标记扩展,采用习惯性的x作为命名空间的前缀

扩    展

含    义

x:Array

代表一个.NET数组。x:Array元素的子元素都是数组元素。它必须与x:Type一起使用,用于定义数组类型

x:Null

表示一个空引用

x:Static

引用在过程式代码中定义的任何一个静态的属性、常量或枚举值。在XAML编译后,这也可以是同一个程序集中的一个非公共成员。如果在默认的命名空间中没有该类型,Member字符串必须有XML命名空间前缀

x:Type

表示System.Type的一个实例,就像C#中的typeof操作符。如果在默认的命名空间中没有该类型,TypeName字符串必须有XML命名空间前缀