编译:XAML与程序代码结合(Compilation: Mixing XAML with Procedural Code)
wpf应用程序完全可以用纯程序代码来实现。而XAML的数据绑定(第九章将介绍)和触发器(第三章将介绍)特性也使得我们能够用纯XAML代码创建简单的应用程序,并且这种松散结构的XAML文件(loose XAML pages)可以被IE来浏览。不过在实际情况中大多数程序都是由两者一起构建的。本节的开始部分将介绍XAML与程序代码结合的两种方法,后面将介绍XMAL命名空间内的关键字,这些关键字使XAML与程序代码能够和好的集成在一起。
运行时装载 解析XAML(Loading and Parsing XAML at Run-Time)
WPF的运行时XAML解析器暴露给我们两个位于System.Windows.Markup命名空间里的类:XamlReader和XamlWriter类。这两个API非常简单。XamlReader类包含一个静态方法Load,该方法包含3个重载版本;XamlWriter类有一个静态方法Save,该方法有5个重载版本。有了这两个类,任何.NET语言使用XAML了。
XamlReader
XamlReader的Load方法有许多重载,用于解析XAML,生成.NET中的对象,并返回根元素的实例。假设当前目录下有名为MyWindow.xaml的文件,该文件的根元素是一个Window对象(第七章的Structing and Deploying an Application将会解释),我们可以用下面的代码来获得这个Window对象:
Window window = null;
using (FileStream fs =new FileStream(“MyWindow.xaml”, FileMode.Open, FileAccess.Read))
{
// Get the root element, which we know is a Window
window = (Window)XamlReader.Load(fs);
}
using (FileStream fs =new FileStream(“MyWindow.xaml”, FileMode.Open, FileAccess.Read))
{
// Get the root element, which we know is a Window
window = (Window)XamlReader.Load(fs);
}
上面的代码中,XamlReader.Load方法使用FileStream的实例作为其输入参数。方法执行完后,Xaml文件中对象已经装载到了内存中,之后XAML文件就没有用处了,可以将它对应的文件流关闭文:当代码执行到using括号的外围后,FileStream会自动关闭。之前说过,XamlReader的Load方法有多种重载,所以我们可以将任意一个Stream对象(或者System.Xml.XmlReader对象)作为Load的输入参数,用起来非常灵活。
Tip
XamlReader类也定义了异步方法,可以异步解析XAML中的内容。我们可以在XamlReader对象的实例里调用它们。如果在读取一个大文件时要保持用户UI的响应性,就可以使用异步读取的方法。和异步读取方法匹配的还有一个CancelAsync方法,用于停止读取操作。XamlReader还定义了LoadCompleted事件,在读取完成后会触发该事件
Tip
XamlReader类也定义了异步方法,可以异步解析XAML中的内容。我们可以在XamlReader对象的实例里调用它们。如果在读取一个大文件时要保持用户UI的响应性,就可以使用异步读取的方法。和异步读取方法匹配的还有一个CancelAsync方法,用于停止读取操作。XamlReader还定义了LoadCompleted事件,在读取完成后会触发该事件
我们已经获得了根级元素的实例,现在可以通过适当的内容属性或集合属性(关于内容属性和集合部分请参见上一篇译文)得到对象的子元素。下面的代码演示了如何使用这些属性获取子元素(假设Window对象包含了一个StackPanel类型的子元素,并且这个StackPanel对象还包含15个Button按钮)
Window window = null;
using (FileStream fs =new FileStream(“MyWindow.xaml”, FileMode.Open, FileAccess.Read))
{
// Get the root element, which we know is a Window
window = (Window)XamlReader.Load(fs);
}
// Grab the OK button by walking the children (with hard-coded knowledge!)
StackPanel panel = (StackPanel)window.Content;
Button okButton = (Button)panel.Children[4];
引用到Button后,就可以对其进行操作了:添加属性(在xaml中难以实现的逻辑),事件处理 或者添加xaml中不能实现的行为。
我们可以编写代码去操作这些元素。例如查找所有内容为ok字符串的Button元素,但即使是这么简单的任务也都需要大量工作。另外,如果你要为Button元素添加一个图形内容,应该如何确定这些Button呢? 非常幸运,我们可以XAML为元素命名,这样可以通过名称来找到相应的元素。
命名xaml元素(Naming XAML Elements)
XAML的命名空间包含一个"Name"关键字,该关键字用来给元素命名。对于刚才ok Button,您可以把Name关键字应用到Button的声明中:
<Button x:Name=”okButton”>OK</Button>
有了这个功能,就可以使用Window.FindName方法来检索元素了:
我们可以编写代码去操作这些元素。例如查找所有内容为ok字符串的Button元素,但即使是这么简单的任务也都需要大量工作。另外,如果你要为Button元素添加一个图形内容,应该如何确定这些Button呢? 非常幸运,我们可以XAML为元素命名,这样可以通过名称来找到相应的元素。
命名xaml元素(Naming XAML Elements)
XAML的命名空间包含一个"Name"关键字,该关键字用来给元素命名。对于刚才ok Button,您可以把Name关键字应用到Button的声明中:
<Button x:Name=”okButton”>OK</Button>
有了这个功能,就可以使用Window.FindName方法来检索元素了:
Window window = null;
using (FileStream fs =new FileStream(“MyWindow.xaml”, FileMode.Open, FileAccess.Read))
{
// Get the root element, which we know is a Window
window = (Window)XamlReader.Load(fs);
}
// Grab the OK button, knowing only its name
Button okButton = (Button)window.FindName(“okButton”);
using (FileStream fs =new FileStream(“MyWindow.xaml”, FileMode.Open, FileAccess.Read))
{
// Get the root element, which we know is a Window
window = (Window)XamlReader.Load(fs);
}
// Grab the OK button, knowing only its name
Button okButton = (Button)window.FindName(“okButton”);
不只是Window对象可以使用FindName方法,FrameworkElement,FrameworkContentElement和许多WPF中重要的基类都包含FindName方法
DIGGING DEEPER
DIGGING DEEPER
Naming Elements Without x:Name
x:Name语法用于为元素命名元素,还有一些类还定义了自己的属性,也用于为元素命名(那些被System.Windows.Markup.RuntimeNamePropertyAttribute修饰的类)。例如,FrameworkElement和FrameworkContentElement就被RuntimeNameProperty("Name")属性修饰。这两个类包含Name属性,我们在为这类元素命名的时候可以使用这个属性了。实际上,我们可以选择任意一种命名方式,但同一时刻只能选择一种!有两种命名的方式多少让人有些困惑,但好处在于:有了Name属性以后我们就可以在程序代码中方便地使用了。而且同一时刻只能选择一种总比同一时刻能选择两种要好,否则会更加困惑。
编译XAML(Compiling XAML)
在运行时装载和解析XAML对于动态换肤(dynamic skinning )非常有意义(第十章Styles, Templates, Skins, and Themes将介绍)。否则.NET就没必要支持XAML了。多数WPF的工程都会使用MSBuild 或VisualStudio完成XAML的编译。XAML的编译需要完成三间事情:1将XAM转化成的二进制形式 2将转换的内容以二进制资源的形式嵌入到将要编译的程序集 3执行连接XAML与程序代码的管道。本书编写时,C#和VB均支持XAML的编译。
在运行时装载和解析XAML对于动态换肤(dynamic skinning )非常有意义(第十章Styles, Templates, Skins, and Themes将介绍)。否则.NET就没必要支持XAML了。多数WPF的工程都会使用MSBuild 或VisualStudio完成XAML的编译。XAML的编译需要完成三间事情:1将XAM转化成的二进制形式 2将转换的内容以二进制资源的形式嵌入到将要编译的程序集 3执行连接XAML与程序代码的管道。本书编写时,C#和VB均支持XAML的编译。
如果您不关心程序代码与XAML文件是如何合成的,那您唯一要做的就是将XAML文件添加到在Visual Studio工程中,然后将它的“生成操作”(Build Action)属性设置为Page即可。将XAML编译并将其和程序代码合成的第一步是在XAML文件的根元素中添加一个子类,这部操作需要用到XAML命名空间内的Class关键字。代码如下:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
x:Class=”MyNamespace.MyWindow”>
…
</Window>
然后将子类定义到一个单独的源文件中(但同属于一个工程),然后添加您需要的成员:
namespace MyNamespace
{
partial class MyWindow : Window
{
public MyWindow
{
// Necessary to call in order to load XAML-defined content!
InitializeComponent();
…
}
Any other members can go here…
}
}
这就是后台文件(code-behind),可以在这里定义XAML代码中涉及的事件处理函数(例如Button的Click)
MyWindow类中的partial关键字至关重要,partial说明这个类的实现是分散到不同文件里完成的。如果.net不支持partial关键字,那么就要在XAML文件中使用Subclass关键字了:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
x:Class=”MyNamespace.MyWindow” x:Subclass=”MyNamespace.MyWindow2”>
…
</Window>
在XAML文件里定义了一个子类(例子中的MyWindow2),而MyWindow作为它的基类。这样就可以以继承的方式来代替了partial的功能。
当我们创建一个c#/vb的WPF工程,或者在添加新项(Add New Item)里选择添加wpf项目,Visual Studio会自动创建XAML文件及其相应后台的文件。XAML文件的根级包含x:Class字符串,而后台文件里包含了局部类的定义.
如果您是个MSBuild用户并且想了解为什么工程允许后代码,可以以记事本的方式打开任意一个WPF的工程文件后可以得到如下的信息:
<ItemGroup>
<Page Include=”MyWindow.xaml”/>
</ItemGroup>
<ItemGroup>
<Compile Include=”MyWindow.xaml.cs”>
<DependentUpon>MyWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
在处理这样一个工程时,build系统在处理MyWindow.xaml文件时会生成如下文件:
1 BAML文件(MyWindow.baml),默认情况下以2二进制资源的形式嵌入到程序集内
2 C#文件(MyWindow.g.cs),将其他源文件包含到程序集内。
namespace MyNamespace
{
partial class MyWindow : Window
{
public MyWindow
{
// Necessary to call in order to load XAML-defined content!
InitializeComponent();
…
}
Any other members can go here…
}
}
这就是后台文件(code-behind),可以在这里定义XAML代码中涉及的事件处理函数(例如Button的Click)
MyWindow类中的partial关键字至关重要,partial说明这个类的实现是分散到不同文件里完成的。如果.net不支持partial关键字,那么就要在XAML文件中使用Subclass关键字了:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
x:Class=”MyNamespace.MyWindow” x:Subclass=”MyNamespace.MyWindow2”>
…
</Window>
在XAML文件里定义了一个子类(例子中的MyWindow2),而MyWindow作为它的基类。这样就可以以继承的方式来代替了partial的功能。
当我们创建一个c#/vb的WPF工程,或者在添加新项(Add New Item)里选择添加wpf项目,Visual Studio会自动创建XAML文件及其相应后台的文件。XAML文件的根级包含x:Class字符串,而后台文件里包含了局部类的定义.
如果您是个MSBuild用户并且想了解为什么工程允许后代码,可以以记事本的方式打开任意一个WPF的工程文件后可以得到如下的信息:
<ItemGroup>
<Page Include=”MyWindow.xaml”/>
</ItemGroup>
<ItemGroup>
<Compile Include=”MyWindow.xaml.cs”>
<DependentUpon>MyWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
在处理这样一个工程时,build系统在处理MyWindow.xaml文件时会生成如下文件:
1 BAML文件(MyWindow.baml),默认情况下以2二进制资源的形式嵌入到程序集内
2 C#文件(MyWindow.g.cs),将其他源文件包含到程序集内。
BAML
BAML含义为二进制应用标记语言(Binary Application Markup Language),BAML是XAML被解析,标记化后的二进制形式.虽然XAML可以由程序代码来实现,但是XAML转换成BAML的过程中不会产生程序代码。所以BAML和MSIL不同,它只是一种被压缩的声明格式,在装载(load)和解析方面要优于XAML,并且文件体积也比XAML小。BAML本身没有对外公布,所以以后版本中的BAML的和现在可能会有所不同。
T I P
BAML含义为二进制应用标记语言(Binary Application Markup Language),BAML是XAML被解析,标记化后的二进制形式.虽然XAML可以由程序代码来实现,但是XAML转换成BAML的过程中不会产生程序代码。所以BAML和MSIL不同,它只是一种被压缩的声明格式,在装载(load)和解析方面要优于XAML,并且文件体积也比XAML小。BAML本身没有对外公布,所以以后版本中的BAML的和现在可能会有所不同。
T I P
x:Class标记只有在XAML被编译时会用到,但在XAML编译时没有x:Class也不会有任何问题。没有x:Class意味这XAML文件不包含对应的后台文件,所以也不能在XAML中使用那些程序代码才有的特性
Generated Source Code
有些代码并不是在XAML编译过程中产生的,这些实际上是一种“黏合代码”(glue code)。这些代码会在装载和解析松散XAML文件。这些代码包含在后缀为.g.cs(或.g.vb)的文件中。单词g表示generated
每个.g文件都包含一个局部类,局部类的类就是在x:Class关键字中涉及的那个类。该局部类包含许多私有字段(原文为: This partial class contains afield (private by default) for every named element in the XAML file。可能是我的理解有误,我发现在.g文件里使用了internal关键字来修饰字段而非private。如果哪位兄台知道原因,请告诉我,谢谢),每个字段都对应这XAML文件中的一个命名的元素,这些元素的名称就是字段的名称。局部类内还包括InitializeComponent方法,该方法需要完成许多工作:装载BAML资源,将类内的字段对应到XAML中声明的元素,注册事件(如果XAML中指定了事件处理)(实际上注册事件这部操作并没有在InitializeComponent中完成,而是在System.Windows.Markup.IComponentConnector.Connect方法里完成的,笔者注)
实际上这些.g文件中的局部类是都是在后台文件类的一部分。对于BAML文件的存在,装载和解析您都不需要关心。您只需要像使用类成员一样使用那些被命名的元素即可,build系统会将他们联系到一起。唯一要记住的是:在构造函数中调用InitializeComponent方法。
DIGGING DEEPER
There Once Was a CAML…
在WPF正式发布之前的早期版本中,WPF可以将XAML编译成BAML或MSIL。这种MSIL叫做CAML,全称是Compiled Application MarkupLanguage. BAML在存储空间上比较有优势,而CAML则是在执行速度上略胜一筹。但最终WPF的开发团段决定使用BAML而抛弃了CAML。因为BAML有几个优势:相对于CAML更加安全,体积更小。
在WPF正式发布之前的早期版本中,WPF可以将XAML编译成BAML或MSIL。这种MSIL叫做CAML,全称是Compiled Application MarkupLanguage. BAML在存储空间上比较有优势,而CAML则是在执行速度上略胜一筹。但最终WPF的开发团段决定使用BAML而抛弃了CAML。因为BAML有几个优势:相对于CAML更加安全,体积更小。
WARNING
Don’t forget to call InitializeComponent in the constructor of your code-behind class!
如果没有调用InitializeComponent方法,就无法使用XAML任何元素(应为相应的BAML没有加载),XAML中的命名元素也都会变成空值。
如果没有调用InitializeComponent方法,就无法使用XAML任何元素(应为相应的BAML没有加载),XAML中的命名元素也都会变成空值。
DIGGING DEEPER
Procedural Code Inside XAML!
实际上我们可以将后台的程序代码内嵌到XAML中(和ASP.NET类似),这是利用了XAML命名空间的Code关键字:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Button Click=”button_Click”>OK</Button>
<x:Code><![CDATA[
void button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
]]></x:Code>
</Window>
XAML文件被编译后,X:Code元素中的内容会被添加到.g.cs文件。我们不一定需要将代码包裹在<![CDATA[…]]>标记内,但使用了<![CDATA[…]]>可以避免<,&的使用。xml解析器不会解析CDATA中的内容,代价就是在CDATA中不能使用]]标记。虽然WPF提供了这个功能,但还是不建议在xaml中添加程序代码。那样会破坏UI与逻辑分离这个特性并且松散xaml也不支持这个功能;Visual Studio也不会为里面的关键字添加关键字应用的颜色
实际上我们可以将后台的程序代码内嵌到XAML中(和ASP.NET类似),这是利用了XAML命名空间的Code关键字:
<Window xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Button Click=”button_Click”>OK</Button>
<x:Code><![CDATA[
void button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
]]></x:Code>
</Window>
XAML文件被编译后,X:Code元素中的内容会被添加到.g.cs文件。我们不一定需要将代码包裹在<![CDATA[…]]>标记内,但使用了<![CDATA[…]]>可以避免<,&的使用。xml解析器不会解析CDATA中的内容,代价就是在CDATA中不能使用]]标记。虽然WPF提供了这个功能,但还是不建议在xaml中添加程序代码。那样会破坏UI与逻辑分离这个特性并且松散xaml也不支持这个功能;Visual Studio也不会为里面的关键字添加关键字应用的颜色
FAQ
Can BAML be decompiled back into XAML?
当然可以。.NET中类的实例可以被序列化成xaml。所以第一步就是取得实例。可以用System.Windows.Application.LoadComponent方法来完成:
System.Uri uri = new System.Uri(“MyWindow.xaml”, System.UriKind.Relative);
Window window = (Window)Application.LoadComponent(uri);
LoadComponent方法与之前使用FileStream读取xaml文件的不同之处在于使用了Uniform Resource Identifier(URI)。得到URI后,LoadComponent会自动从资源中取得BAML(利用MSBuild得到其原始的xaml文件)。实际上Visual Studio自动产生的InitializeComponent方法就就调用Application.LoadComponent方法得到BAML.第八章将介绍使用URI得到资源的一些知识。
当您得到了根元素的实例后,就可以使用System.Windows.Markup.XamlWriter类获得XAML的结构了。XamlWriter类的Save方法共有5个重载。最简单的就是“接收对象实例然后返回字符串”的重载了:
string xaml = XamlWriter.Save(window);
当然可以。.NET中类的实例可以被序列化成xaml。所以第一步就是取得实例。可以用System.Windows.Application.LoadComponent方法来完成:
System.Uri uri = new System.Uri(“MyWindow.xaml”, System.UriKind.Relative);
Window window = (Window)Application.LoadComponent(uri);
LoadComponent方法与之前使用FileStream读取xaml文件的不同之处在于使用了Uniform Resource Identifier(URI)。得到URI后,LoadComponent会自动从资源中取得BAML(利用MSBuild得到其原始的xaml文件)。实际上Visual Studio自动产生的InitializeComponent方法就就调用Application.LoadComponent方法得到BAML.第八章将介绍使用URI得到资源的一些知识。
当您得到了根元素的实例后,就可以使用System.Windows.Markup.XamlWriter类获得XAML的结构了。XamlWriter类的Save方法共有5个重载。最简单的就是“接收对象实例然后返回字符串”的重载了:
string xaml = XamlWriter.Save(window);
BAML很容易被“破解”。在这一点上它和那些运行在本地或者会将UI呈现到本地的软件没什么区别(例如,您可以很轻松地获得一个网页中的html,javascript和css)
XAML关键字(XAML Keywords)
XAML语言命名空间(http://schemas.microsoft.com/winfx/2006/xaml)定义了许多关键字。这些关键字大多数是永安里管理XAML中元素与程序代码的xxxx,也有一小部分有其他用途。那写有其他用途的关键字您之前已经见识(Key,Name,Class,Subclass),不过表2.1也包含这些关键字。它们都以"x"字符串作为前缀出现,这也符合他们在XAML中的形式。
表2.2中列举了XAML命名空间内的扩展标记,为了避免和表2.1中的关键字混淆,我们将它单独列举出来。在使用扩展标记是一般都会将其后缀省略,所以表2.2中也没有将后缀打印出来。