说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
在WPF程序中很少单纯用XAML代码或单纯用传统过程式代码来完成,大多数程序混合使用了这两种代码。本节总结了在程序中控制XAML的方法:
1. 在运行时加载和解析XAML
WPF中有两个类是作为XAML解析器,位于System.Windows.Markup命名空间中:
-
XamlReader,包含了一些对静态Load方法的重载;该方法解析XAML创建合适的.NET对象,返回一个根元素类型的实例。
-
XamlWriter,包含了5个对静态Save方法的重载;Save方法实现由一个根元素的实例得到其XAML的表示。如下代码传入一个window对象,返回一个包含XAML的字符串:
string Xaml = XamlWriter.Save(window);
通过这两个类你可以在运行时使用XAML。
下面给出一个示例:
以下代码加载当前目录下一个名为MyWindow.xaml的XAML文件,其包含一个window对象作为根节点。这段代码解析此XAML,获取window对象的引用并访问其子元素/子对象及属性。
Window window = null; using (FileStream fs = new FileStream("MyWindow.xaml", FileMode.Open, FileAccess.Read)) { //获取根元素,一个Window对象 window = (Window)XamlReader.Load(fs); } //遍历得到子元素中OK按钮 StackPanel panel = (StackPanel)window.Content; Button okButton = (Button)panel.Children[4];
当然,仅仅使用加载XAML文件的方式进行上面展示的访问元素等操作就小题大做了,使用这种方式多为完成一些XAML代码难以或无法完成的操作,如添加事件处理程序或调用方法。通过给元素命名,可以更方便的在代码中操作加载的XAML的元素。这归功于WPF的Window元素提供了FindName方法来搜索其子元素并返回找到的元素的实例。
这种动态加载的方式另一个作用是动态使用XSLT建立XML(XAML),交由WPF加载,这是一种动态建立用户界面的方式(数据绑定是更好的方式)。
如为Button添加x:Name="okButton"属性后,可以使用如下代码替换前文代码:
Button okButton = (Button)window.FindName("okButton");
在WPF类中也有其它像FrameworkElement, FrameworkContentElement等类也提供了FindName方法。
另外有一部分WPF类,如FrameworkElement与FrameworkContentElement,标记了位于System.Windows.Markup下的RuntimeNamePropertyAttribute。此Attribute可以将指定给其的参数为名称的属性作为元素的名称。由于FrameworkElement与FrameworkContentElement被标记为[RuntimeNameProperty("Name")],所以FrameworkElement与FrameworkContentElement可以通过Name属性来设置名称,而无需使用x:Name语法。类可以使用任意一种机制来设置名称,但不能同时使用两者。
另外,XamlReader也定义了LoadAsync实例方法用于异步加载与解析XAML内容。这通常用于加载较大的文件或位于网络上的一个位置。与其配合使用的有CancelAsync方法用于停止处理,LoadCompleted事件在加载完成时通知程序。
编译XAML
XAML编译有三件事:
-
将一个XAML文件转换为一种特殊的二进制文件。
-
将转好的内容作为二进制资源嵌入到正在被创建的程序集。
-
执行链接操作,将XAML与过程式代码自动连接起来。
为了编译一个XAML并让其可以与过程式代码交互,就像VS的WPF模板创建出的.xaml文件与.xaml.cs文件之间可以交互一样,需要给XAML文件中的根元素指定一个子类,这可以用XAML的关键字x:Class来设置(正如第一篇所介绍)。
示例(注意粗体部分):
<Window x:Class="WpfApplication1.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> </Window>
过程代码文件:
namespace WpfApplication1 { public partial class Window1 : Window { public Window1() { InitializeComponent(); } } }
以上代码中partial关键字可以将一个两部分分别位于XAML和C#代码中的类合在一起。在用户添加XAML时,VS生成两个文件并会自动完成添加x:Class的过程。这种方式与ASP.NET WebForm模型中将.aspx页面与.aspx.cs隐藏代码分开的方式极其相似。
在不支持部分类特性(partial关键字)的语言中,需要使用ASP.NET1.1(那时的C#1.0版不支持部分类)中处理此问题的方法。即在XAML中再声明一个子类,并将代码文件中的代码定义在这个子类中。
示例:
<Window x:Class="WpfApplication1.Window1" x:SubClass="WpfApplication1.Window1Child" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300"> </Window>
代码中x:SubClass关键字定义的Window1Child作为Window1的子类,表现在过程代码中如下(使用支持部分类的C#,仅供演示):
namespace WpfApplication1 { public partial class Window1Child : Window1 { public Window1Child() { InitializeComponent(); } } }
这样通过继承模拟了把代码分散在两个文件中。
通过以上分析可以看出,在代码隐藏文件中调用InitializeComponent很关键。这个函数主要完成:建立由XAML中元素所设定的对象树状列表;需要为属性赋值;需要为命名元素生成字段;绑定所有特定的事件处理。当需要为了传递参数等目的重载构造函数后,需要注意调用InitializeComponent方法或调用无参的构造函数。
在.xaml与.xaml.cs文件被编译的过程中:
-
XAML被生成为一个BAML文件,作为二进制资源嵌入到程序集中。BAML为Binary Application Markup Language的缩写,意为二进制应用程序标记语言。在XAML生成BAML的过程中没有生成任何过程代码,只是进行了一种格式的压缩,使其体积比XAML要小。这是为了减少运行时的消耗。(被添加到VS的纯XAML(无x:Class)被编译为BAML,并由Parser.LoadBaml方法来加载,生成对应于源XAML的对象树状显示。)
-
隐藏代码文件.xaml.cs在未被编译的情况下以粘贴方式放入.g.cs文件中(g含义为generated),每个.g.cs文件都有一个以XAML文件中x:Class关键字定义的类名为名称的部分类,其中还存放了一些以XAML中命名元素为名的私有成员,另外在.xaml.cs中看不到定义的InitialCompenent方法的定义也存在于.g.cs文件中。在最终生成程序时.g.cs文件会被编译到程序集中。
当然如果一个XAML不需要使用过程式代码,则不需要使用x:Class指定类,只需要将其添加到项目中,其会被编译成高效的BAML文件并被使用。
另外,XAML还支持代码嵌入,这使用XAML命名空间的Code关键字实现:
示例代码:
<x:Code> <![CDATA[ void button_Click(object sender, RoutedEventArgs e) { this.Close(); } ]]> </x:Code> |
其中的过程代码在编译时被放到.g.cs文件中。
另外,把代码放置于<![CDATA[…]]>嵌入不是必需的,其可以避免使用'<'替换'; ',或者使用'&'替换'&'。CDATA允许非XML的文本逐字的插入,因为XML解析器会忽略CDATA节。使用<![CDATA[…]]>需要注意避免在代码中使用']',这个符号会终结CDATA节。这种“代码嵌入”特性尽量不要使用,其在VS中没有语法高亮与智能感知的支持。
BAML的反编译
可以在过程代码中使用如下代码来加载BAML,VS也是使用下面函数的另一个重载供InitializeComponent方法来加载BAML。
示例代码:
System.Uri uri = new System.Uri("MyWindow.xaml", System.UriKind.Relative); Window window = (Window)Application.LoadComponent(uri);
在参数uri实例构造函数中指定的是一个xaml文件的名称,然后LoadComponent会找到对应的此xaml文件编译后作为嵌入资源的BAML。
如果XAML中没有类定义(x:Class关键字),有两种方式可以以树状方式展示,既可以在运行时编译XAML,也可以调用BAML将XAML预编译成二进制格式后导入,无论怎样,我们都可使用System.Windows.Serialization命名空间中的Parse类来建立对象的树状表示。这是解释XAML的方式,如果XAML中含有x:Class,则必须用XAML编译器编译XAML。
参考:
《WPF揭秘》