导读:
本文转自
http://book.csdn.net/bookfiles/591/10059119378.shtml
第19章 XAML(和Camel押韵)
下面是一个合法的Extensible Markup Language(XML)片段:
这3行组成了一个XML element:开始标签(start tag)、结束标签(end tag)和两者中间的内容。这里的element类型是Button。开始标签包含两个attribute说明(specification),其attribute名称是Foreground和FontSize。它们被指定了attribute值,XML规定要把attribute值放在一对单引号或双引号内。在开始标签和结束标签之间是element内容,在本例中,是某种字符数据(character data,这是XML的术语)。
XML被设计成一般目的的标记语言,应用相当广,而Extensible Application Markup Language(XAML)是其中的一个应用。
XAML(发音为“zammel”)是WPF补充的编程界面。你可能也已经预料到,上面的XML片段也是XAML的合法片段。Button是定义在System.Windows.Controls命名空间的类,而Foreground和FontSize都是该类的property。你将“Hello, XAML!”文字指定为此Button对象的Content property。
XAML的设计主要是为了对象的建立与初始化。上面的XAML片段对应着下面这段等价(但是更多字)的C#程序代码:
Button btn = new Button();
btn.Foreground = Brushes.LightSeaGreen;
btn.FontSize = 32;
btn.Content = "Hello, XAML!"
请注意:XAML不需要我们明确指出LightSeaGreen是Brushes类的成员,而且我们可以用字符串 "24pt" 来表示24 points。印刷上的point是1/72英寸,所以24 points对应到32设备无关单位。虽然XML常常会更冗长(而XAML在某些方面更是变本加厉),但XAML常常会比等价的程序代码更精要。
WPF程序窗口的layout常常是面板、控件、element的层次结构。这种层次结构和XAML内部嵌套的element相互辉映:
在此XAML片段中,StackPanel有3个孩子:一个Button、一个Ellipse和另一个Button。第一个Button具有文字内容。另一个Button具有Image内容。注意,Ellipse和Image没有内容,所以这两个element可以用特殊的XML empty-element语法来表示:将整个end tag用一个斜线取代,此斜线放在start tag中关闭tag的大于符号前面。也请注意,Image element的Stretch attribute被指定为Stretch枚举的某个成员的时候,直接写成员名称(而没有写Stretch枚举类型)。
XAML文件本身常常取代Window派生类的整个构造函数,这种构造函数常常用来进行layout并且设置事件处理函数。事件处理函数本身必须用程序代码编写(例如C# 语言)。然而,如果你可以用数据绑定取代事件处理函数,该绑定通常会写在XAML中。
使用XAML可以将一个应用程序的视觉表现和功能分离。此分离让设计者可以操控XAML文件,建立有吸引力的用户界面,而程序员可以专注于运行时element与控件的交互。可以产生XAML的设计工具已经开始出现在市面上。
即使程序员没有必要和图形设计艺术家合作,Visual Studio也内置了设计工具,可以产生XAML。明显地,设计工具产生XML会比产生C# 程序代码更受欢迎。以前Visual Studio设计工具就是产生Windows Forms的程序代码。设计工具产生程序代码之后还必须读进这些程序代码,这通常要求程序代码遵照某种格式。因此,人类程序员不可以弄乱这些被产生出来的程序。然而,XML被特别地设计成既适合计算机编辑,也适合人类编辑。只要编辑者最后让XAML处于语法正确的状态,就不会有问题。
尽管设计工具可以产生XAML,你身为程序员,还是要学习XAML的语法。最好的学习方式,就是从实践中学习。我相信每个WPF程序员都需要能够流畅地使用XAML,并且可以手工编写XAML,我会告诉你如何做到。
本章到目前为止,展示出来的XAML的片段都是取自某个XAML文件中的一小部分,但它们并不足以独立存在。这里存在相当不明确。Button element是什么?是衬衫钮扣(shirt button)吗?电子按钮(electrical button)?还是选举胸章(campaign button)?XML文件必须相当明确才行,不可以有不清楚的地方。如果两个XML文件使用相同的element名称代表不同的意义,这两种文件必须能够被区分开才行。
为此,我们需要XML命名空间。WPF程序员所建立的一份XAML文件,和衬衫钮扣制造商所建立的XML文件,两者具有不同的命名空间。
你在文件中利用xmlns attribute声明默认的XML命名空间。此命名空间会被应用于声明出现的element以及其下的每个孩子。XML命名空间的名称必须是唯一且一致的,为此常常使用URL作为命名空间。对于WPF的XAML命名空间,此URL是:
http://schemas.microsoft.com/winfx/2006/xaml/presentation
别试着用浏览器打开此URL,你看不到东西的。这只是微软的一个命名空间,用来辨识XAML的诸多element之用(例如Button、StackPanel、Image)。
只要加入xmlns attribute和适当的命名空间,本章开头所展示的XAML片段就可以变成一个功能完整的XAML文件:
此XAML现在可以被放进一个小文件中,或许通过Notepad或NotepadClone建立此文件,或许在上面加上XML注释来帮助文件的识别。你可以将下面的文件保存到你的硬盘。
XamlButton.xaml
这里使用灰色的背景,表示此文件是本书源代码的一部分,你可以从Microsoft Press的网站下载。如果你不想自己键入,你可以在“Chapter 19”的目录下找到此文件。
不管你如何得到此文件,如果你在操作系统上安装了WinFx .NET扩展(extensions to .NET),或者你的操作系统是Microsoft Vista,你就可以像一般程序一样,只要在Windows Explorer中对它双击,或者你也可以在命令提示下执行它。你将会看到Microsoft Internet Explorer(IE)出现,此按钮将会填满整个IE的客户区(不过上面还是会有一些工具栏和URL地址栏)。工具栏的导览按钮会被disable,因为从这样的内容中,没有地方可以前往。(如果你的操作系统是Microsoft Vista,导览按钮将不会出现在客户区;它们的功能被纳入IE自己的导览按钮中。)
像XamlButton.xaml这样的文件,被称为“松散”XAML或“独立”XAML。.xaml扩展名(file name extension)被关联到PresentationHost.exe程序。只要执行XAML,就会造成PresentationHost.exe执行,此程序负责建立Page类型的对象(此类继承自FrameworkElement,但是某些地方很类似于Window),而此程序又可以被嵌入Internet Explorer。PresentationHost. exe程序还将加载的XAML转成实际的Button对象,并将对象设定成Page的Content property。
如果XAML有错误,IE会告诉你,你可以点击IE的“More information”按钮,这个时候你会看到PresentationHost.exe也存在此stack trace(调用堆栈)中。在此stack trace的许多方法中,你可以找出一个特定的静态方法,名为XamlReader.Load,属于System.Windows.Markup命名空间。正是此方法将XAML转成对象,我稍后会告诉你如何使用它。
除了从你自己的硬盘直接执行XamlButton.xaml,你也可以将此文件放在你的网站上,从那里执行。然而,你可能需要为.xaml扩展名注册MIME类型。在某些服务器上,你可以将下面这行加入.htaccess文件中:
AddType application/xaml+xml xaml
下面是另一个“独立”的XAML文件,建立具有3个孩子的StackPanel,这3个孩子分别为Button、Ellipse、ListBox。
XamlStackPanel.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Stroke="Red" StrokeThickness="10" />
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
XML文件只能够有一个root element,在此文件中,root element是StackPanel。StackPanel的“开始tag”和“结束tag”之间,是StackPanel的内容(它的3个孩子)。Button element和你以前看过的相当类似。此Ellipse element包含5个attribute(对应Ellipse类的property),但不具备content,所以它使用empty-element的语法(将“开始tag”和“结束tag”合并成一个)。ListBox element具有7个孩子,都是ListBoxItem element。每个ListBoxItem的内容是文字字符串。
一般来说,XAML文件代表完整的element树。当PresentationHost.exe加载一个XAML文件,不仅把树中每个element创建出来并初始化,而且将这些element组成视觉树。
当执行这些独立的XAML文件,你可能注意到IE的标题栏会显示文件的路径。在实际的应用程序中,你可能想控制这里显示的文字。你可以让一个root element变成一个Page,设定WindowTitle property,并让StackPanel成为Page的孩子,如同下面的独立XAML文件的做法。
XamlPage.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
WindowTitle="Xaml Page">
Stroke="Red" StrokeThickness="10" />
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
你可能会想要尝试将Window当作一个独立XAML文件的root element。这是行不通的,因为PresentationHost.exe想让root element成为“某东西”的孩子,而Window对象不能当任何东西的孩子。独立XAML文件的root element可以是继承自FrameworkElement的任何类,但是不可以是Window。
假设你有一个C# 程序,定义一个字符串变量(例如strXaml),此变量内容是一个小而完整的XAML文件:
string strXaml =
"";
为了让字符串更具有可读性,我使用单引号而非双引号来表示attribute的值。你可以写一个程序,解析(parse)此字符串,以建立并初始化一个Button对象吗?你会使用许多reflection,并且根据用来设定Foreground与FontSize property的数据,做出某些假设。这样的parser做法不难想象,所以如果说已经存在这样的parser,应该也不会让你觉得惊讶。System.Windows.Markup命名空间包含XamlReader类,它具有一个名为Load的静态方法,可以解析XAML,并将它转成一个初始化过的对象。(除此之外,静态的XamlWriter.Save方法做的事刚好是反方向,从对象产生XAML。)
WPF程序可以使用XamlReader.Load将一段XAML转成一个对象。如果此XAML的根element具有子element,这些element会一并被转换,并放在视觉树中,符合XAML的层次结构。
要使用XamlReader.Load,你当然要加入System.Windows.Markup命名空间,但是你也需要引用System.Xml.dll组件(assembly),它包含XML相关的类,这些类显然是XamlReader.Load需要用到的。不幸的是,XamlReader.Load不能直接接受字符串作为参数。否则,你就可以直接传递一些XAML到此方法,并将结果转换成所需要的对象类型:
Button btn = (Button) XamlReader.Load(strXaml); // Won’t work!
XamlReader.Load需要一个Stream对象,或者一个XmlReader对象。这里有一个做法,是利用MemoryStream对象。(你需要将System.IO namespace命名空间加入程序代码中。)StreamWriter将字符串写进MemoryStream中,然后将MemoryStream传入XamlReader.Load中:
MemoryStream memory = new MemoryStream(strXaml.Length);
StreamWriter writer = new StreamWriter(memory);
writer.Write(strXaml);
writer.Flush();
memory.Seek(0, SeekOrigin.Begin);
object obj = XamlReader.Load(memory);
下面是比较流畅的做法,需要使用System.Xml与System.IO命名空间:
StringReader strreader = new StringReader(strXaml);
XmlTextReader xmlreader = new XmlTextReader(strreader);
object obj = XamlReader.Load(xmlreader);
你可以使用下面这样的做法,这条语句相当难以阅读:
object obj = XamlReader.Load(new XmlTextReader(new StringReader(strXaml)));
下面的程序定义了和上面一样的strXaml字符串,然后将这一短的XAML文件转成一个对象,并将对象设定成为窗口的Content property:
LoadEmbeddedXaml.cs
//-------------------------------------------------
// LoadEmbeddedXaml.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Xml;
namespace Petzold.LoadEmbeddedXaml
{
public class LoadEmbeddedXaml : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new LoadEmbeddedXaml());
}
public LoadEmbeddedXaml()
{
Title = "Load Embedded Xaml";
string strXaml =
"";
StringReader strreader = new StringReader(strXaml);
XmlTextReader xmlreader = new XmlTextReader(strreader);
object obj = XamlReader.Load(xmlreader);
Content = obj;
}
}
}
因为此程序刚好知道从XamlReader.Load返回的对象是Button,所以就将它转成Button:
Button btn = (Button) XamlReader.Load(xmlreader);
然后此程序会将事件处理函数设置好:
btn.Click += ButtonOnClick;
如果你的程序包含明确的代码建立并初始化此Button的话,你可以对它做任何事。
当然,将XAML定义成字符串变量,有一点诡异。或许比较好的做法是让XAML成为运行时从程序可执行文件的资源中加载的对象。
我们从一个空的工程开始(就和平常一样),将此工程命名为LoadXamlResource。加入对System.Xml组件和其他WPF组件的引用。从“Project”菜单选取“Add New Item”(或者用鼠标右键点击工程名称,然后选取“Add New Item”)。选择一个XML File的template,文件名为LoadXamlResource.xml。(我要向你展示,这里扩展名为.xml会比扩展名为.xaml更容易。如果你使用.xaml扩展名,Visual Studio会想要加载XAML设计工具,并做出一些目前不太适合的假设。)下面是XML文件。
LoadXamlResource.xml
http://schemas.microsoft.com/winfx/2006/xaml/presentation">
Height="100"
Margin="24"
Stroke="Red"
StrokeThickness="10" />
Height="100"
Margin="24">
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
如你所见,此文件非常类似于独立的XamlStack.xaml文件。最大的区别是,我在此为Button对象加入了Name attribute。此Name property是由FrameworkElement所定义的。
很重要的是:鼠标右键点击Visual Studio中的LoadXamlResource.xml文件,选择Properties,确定Build Action被设定成Resource,否则此程序无法将它视为资源而加载。
LoadXamlResource工程也包含一个看起来更正常的C# 文件,这里有一个继承自Window的类。
LoadXamlResource.cs
//-------------------------------------------------
// LoadXamlResource.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace Petzold.LoadXamlResource
{
public class LoadXamlResource : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new LoadXamlResource());
}
public LoadXamlResource()
{
Title = "Load Xaml Resource";
Uri uri = new Uri("pack://application:,,,/LoadXamlResource.xml");
Stream stream = Application.GetResourceStream(uri).Stream;
FrameworkElement el = XamlReader.Load(stream) as FrameworkElement;
Content = el;
Button btn = el.FindName("MyButton") as Button;
if (btn != null)
btn.Click += ButtonOnClick;
}
void ButtonOnClick(object sender, RoutedEventArgs args)
{
MessageBox.Show("The button labeled '" +
(args.Source as Button).Content +
"' has been clicked");
}
}
}
构造函数为此XML资源建立一个Uri对象,然后使用静态的Application.GetResourceStream property,取得一个StreamResourceInfo对象。StreamResourceInfo包含一个名为Stream的property,此property返回一个Stream对象,正是此资源。将此Stream对象用作XamlReader.Load的参数,得到一个StackPanel对象,指定给窗口的Content property。
一旦XAML所转成的对象变成窗口视觉树的一部分,就可以使用FindName方法在树中找出特定名称的element,也就是此Button。然后此程序就会为它设置事件处理函数,或做些别的事。想为运行时加载的XAML设置事件处理函数,这或许是最直接的做法。
下面是一个小变化,此工程名为LoadXamlWindow。就像是前面的工程一样,此XML文件必须将Build Action设定为Resource:
LoadXamlWindow.xml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Load Xaml Window"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
Height="100"
Margin="24"
Stroke="Red"
StrokeThickness="10" />
Height="100"
Margin="24">
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
此XAML的root element是Window。请注意,Window开始标签(start tag)的attribute包括了Title、SizeToContent和ResizeMode。其中SizeToContent和ResizeMode的值是各自相关的枚举成员。
独立的XAML文件,其根element不可以是Window,因为PresentationHost.exe希望让被转换的XAML成为某东西的孩子(而Window不可以作为孩子)。幸运的是,下面的程序知道XAML资源是一个Window对象,所以它没有继承自Window,或者直接创建一个Window对象(而是将此XAML资源作为Window对象)。
LoadXamlWindow.cs
//-----------------------------------------------
// LoadXamlWindow.cs (c) 2006 by Charles Petzold
//-----------------------------------------------
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
namespace Petzold.LoadXamlWindow
{
public class LoadXamlWindow
{
[STAThread]
public static void Main()
{
Application app = new Application();
Uri uri = new Uri("pack://application:,,,/LoadXamlWindow.xml");
Stream stream = Application.GetResourceStream(uri).Stream;
Window win = XamlReader.Load(stream) as Window;
win.AddHandler(Button.ClickEvent,
new RoutedEventHandler(ButtonOnClick));
app.Run(win);
}
static void ButtonOnClick(object sender, RoutedEventArgs args)
{
MessageBox.Show("The button labeled '" +
(args.Source as Button).Content +
"' has been clicked");
}
}
}
Main方法建立一个Application对象,加载XAML,然后将XamlReader.Load返回的对象转型成为Window对象。此程序为按钮Click事件设置一个事件处理函数。这里并没有从视觉树中查找按钮,而是调用窗口的AddHandler方法,来设置事件处理函数。最后,Main方法将此Window对象传递给Application的Run方法。
下面的程序包含一个Open File对话框,让你从磁盘加载一个XAML文件。你可以使用此程序,加载本章至今的任何XAML文件(扩展名是.xml)。
LoadXamlFile.cs
//---------------------------------------------
// LoadXamlFile.cs (c) 2006 by Charles Petzold
//---------------------------------------------
using Microsoft.Win32;
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Xml;
namespace Petzold.LoadXamlFile
{
public class LoadXamlFile : Window
{
Frame frame;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new LoadXamlFile());
}
public LoadXamlFile()
{
Title = "Load XAML File";
DockPanel dock = new DockPanel();
Content = dock;
// Create button for Open File dialog.
Button btn = new Button();
btn.Content = "Open File...";
btn.Margin = new Thickness(12);
btn.HorizontalAlignment = HorizontalAlignment.Left;
btn.Click += ButtonOnClick;
dock.Children.Add(btn);
DockPanel.SetDock(btn, Dock.Top);
// Create Frame for hosting loaded XAML.
frame = new Frame();
dock.Children.Add(frame);
}
void ButtonOnClick(object sender, RoutedEventArgs args)
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.Filter = "XAML Files (*.xaml)|*.xaml|All files (*.*)|*.*";
if ((bool)dlg.ShowDialog())
{
try
{
// Read file with XmlTextReader.
XmlTextReader xmlreader = new XmlTextReader(dlg.FileName);
// Convert XAML to object.
object obj = XamlReader.Load(xmlreader);
// If it's a Window, call Show.
if (obj is Window)
{
Window win = obj as Window;
win.Owner = this;
win.Show();
}
// Otherwise, set as Content of Frame.
else
frame.Content = obj;
}
catch (Exception exc)
{
MessageBox.Show(exc.Message, Title);
}
}
}
}
}
正如你在ButtonOnClick方法中所看到的,从OpenFileDialog取出文件名,会比将XAML当作资源加载更容易一些。文件名可以被直接传递给XmlTextReader构造函数,此对象(XmlTextReader)可以被XamlReader.Load当参数接受。
如果从XamlReader.Load返回的对象是Window的话,此方法有一些特殊的逻辑。它将Window对象的Owner property设定成自己,然后调用Show,彷佛被加载的窗口是一个modeless对话框。(我发现关闭主应用程序窗口,但是XAML加载的窗口依然没关闭,造成应用程序无法结束。当我发现这种情况之后,才加入设定Owner property的程序代码。这样的做法对我来说不太适当。另一种解决方式是设定Application的ShutdownMode property为ShutdownMode. OnMainWindowClose,下一章的XAML Cruncher程序就是这么做的。)
你现在已经看到一些不同的做法,都可以在运行时加载XAML,而且你也知道加载XAML的程序代码如何能够定位树中的各种element,并为它们设置事件处理函数。
然而,在实际的应用程序中,将XAML和你的源代码一起编译,这么做比较常见。毫无疑问,这样更有效率,而且你可以在编译版本的XAML中做某些事,这些事却不能在独立的XAML中进行。其中一些事,就是在XAML中指定事件处理函数的名称。通常事件处理函数
本身位于某个程序代码(procedure code)文件中,但是你也可以将某些C# 程序代码嵌入XAML内。当你将XAML和整个工程一起编译时,是可以这么做的。通常你的工程中,应用程序的每个页面或每个窗口(包括对话框)都会有一个XAML文件,而且每个XAML文件都会有一个关联的程序代码文件(常常称为code behind文件)。但是这只是通则,你也可以在你的工程中使用更多或更少的XAML文件。
你到目前为止所看见的XAML都使用WPF的类和property。但是,XAML并非WPF专用的标记语言。应该把WPF当作是XAML的一种可能的应用方式。XAML也可以被用在WPF以外的其他应用程序框架(比方说,XAML可以搭配Windows Workflow Foundation使用。)
XAML规范定义了数个element和attribute,你可以在任何XAML应用(包括WPF)中使用。这些element和attribute被关联到不同于WPF的命名空间,而如果你想要使用XAML专用的element和attribute(你一定会这么做的),那么你需要将第二个命名空间的声明放在你的XAML文件中。这第二个命名空间的声明引用下面的URL:
http://schemas.microsoft.com/winfx/2006/xaml
这和WPF的URL一样,但是没有presentation路径的部分,因为presentation代表的是WPF。此WPF命名空间的声明将会持续出现在本书所有的XAML文件中:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
XAML专用element与attribute的命名空间习惯上被声明成“x”前缀(prefix):
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
当然,你可以使用任何前缀(但是开头不可以是XML),不一定要用x,不过x已经变成许多XAML文件所采用的惯例了。
(理论上,让XAML文件内默认的命名空间和XAML自己的关联起来,然后用第二个命名空间声明WPF element,这样感觉更加合理。然而,XAML所定义的element和attribute相当少,为了避免XAML文件内有太多前缀,让WPF element的命名空间为默认命名空间,会比较实用。)
在本章中,你会看到Class attribute和Code element的例子,两者都是属于XAML命名空间,而非WPF命名空间。因为XAML命名空间习惯上使用x当前缀,所以Class和Code element出现在XAML文件中,通常会是“x:Class”与“x:Code”,我正是这么称呼它们的。
x:Class attribute只能出现在XAML文件的root element。此attribute只允许会被编译成工程一部分的XAML使用。它不可以出现在松散的XAML或运行时加载的XAML中。x:Class属性看起来类似这样:
x:Class="MyNamespace.MyClassName"
常常,此x:Class attribute会出现在Window的root element,所以此XAML文件的整体结构可能是:
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="MyNamespace.MyClassName"
... >
...
这里的MyNamespace命名空间指的是和此应用程序工程有关联的.NET命名空间(也就是CLR命名空间)。你通常会有对应的Window类(用C# 语言写的),具有相同的命名空间和类,定义此类时使用partial关键字:
public namespace MyNamespace
{
public partial class MyClassName: Window
{
...
}
}
这就是code-behind文件,因为它包含code(这里的code,常常是事件处理函数,也可能是某些初始化的程序代码),这些code是用来支持定义在XAML文件内的控件和element。此XAML文件和此code-behind文件其实是同一个类的不同部分,Window类通常如此。
再一次,让我们从一个空的工程开始。此次的工程名为CompileXamlWindow。此工程需要两个文件,一个名为CompileXamlWindow.xaml的XAML文件,和一个名为CompileXaml- Window.cs的C# 文件。两个文件都是相同类的不同部分,此类的全名(含命名空间)为Petzold.CompileXamlWindow.CompileXamlWindow。
让我们先建立此XAML文件。在空的工程中,加入一个XML文件的新项目,指定名称为CompileXamlWindow.xaml。Visual Studio会加载设计工具,但是你要试着摆脱设计工具。在源代码窗口的左下角,点击Xaml页,而非点击Design页。
如果你检查CompileXamlWindow.xaml文件的Properties,Build Action应该是Page,如果不是的话,就设定成Page。(稍早在LoadXamlResource和LoadXamlWindow工程中,我请你把XAML文件的扩展名设为.xml,现在我却要你用.xaml当扩展名。其实,使用什么扩展名无所谓,重点在Build Action的设定。在前面的工程中,我们想要让XAML文件变成可执行文件的资源;在现在的项目,我们想要让此文件被编译,而将Build Action设定成Page就会造成此文件被编译。)
CompileXamlWindow.xaml文件类似于LoadXamlWindow.xml文件。第一个大的差异在于此文件包含第二个命名空间x前缀的声明,以及在根element中使用x:Class attribute。我们这里所定义的是一个类,继承自Window,此类的完整名称是Petzold.CompileXamlWindow. CompileXamlWindow。
CompileXamlWindow.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Petzold.CompileXamlWindow.CompileXamlWindow"
Title="Compile XAML Window"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
Width="200"
Height="100"
Margin="24"
Stroke="Black"/>
Width="150"
Height="150"
Margin="24"
SelectionChanged="ListBoxOnSelection" />
实际上,你将会看到,此XAML文件定义了一个类,其C# 语法看起来类似下面:
namespace Petzold.CompileXamlWindow
{
public partial class CompileXamlWindow: Window
{
...
}
}
Partial关键词表示此CompileXamlWindow类在其他地方还有程序代码。那是在C# code-behind文件中的程序代码。
请注意按钮的XAML element包含了一个Click事件的attribute,并指定事件处理函数的名称为ButtonOnClick。这个事件处理函数在哪里?它将会在CompileXamlWindow类的C# 程序代码部分。ListBox也需要SelectionChanged事件的处理函数。
虽然Ellipse和ListBox都具有Name attribute,其值分别为elips和lstbox。你稍早看到程序要如何利用FindName方法定位树中的element。当你编译XAML的时候,Name attribute扮演着相当重要的角色。它们会变成类的字段,所以用XAML所建立的类,在编译期间,更像是这样:
namespace Petzold.CompileXamlWindow
{
public partial class CompileXamlWindow: Window
{
Ellipse elips;
ListBox lstbox;
...
}
}
在你所编写的CompileXamlWindow类C# 部分,你可以直接引用这些字段。下面是code-behind文件,包含了CompileXamlWindow类剩下的部分:
CompileXamlWindow.cs
//--------------------------------------------------
// CompileXamlWindow.cs (c) 2006 by Charles Petzold
//--------------------------------------------------
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.CompileXamlWindow
{
public partial class CompileXamlWindow : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new CompileXamlWindow());
}
public CompileXamlWindow()
{
// Required method call to hook up event handlers and
// initialize fields.
InitializeComponent();
// Fill up the ListBox with brush names.
foreach (PropertyInfo prop in typeof(Brushes).GetProperties())
lstbox.Items.Add(prop.Name);
}
// Button event handler just displays MessageBox.
void ButtonOnClick(object sender, RoutedEventArgs args)
{
Button btn = sender as Button;
MessageBox.Show("The button labled '" + btn.Content +
"' has been clicked.");
}
// ListBox event handler changes Fill property of Ellipse.
void ListBoxOnSelection(object sender, SelectionChangedEventArgs args)
{
ListBox lstbox = sender as ListBox;
string strItem = lstbox.SelectedItem as string;
PropertyInfo prop = typeof(Brushes).GetProperty(strItem);
elips.Fill = (Brush)prop.GetValue(null, null);
}
}
}
CompileXamlWindow类继承自Window,这和平常一样,但是声明也包含了partial关键词。此类具有一个静态的Main方法,这也和平常一样。然而,CompileXamlWindow构造函数一开始会调用InitializeComponent。此方法显然是CompileXamlWindow类的一部分,但是在此文件中却看不到此方法的定义。你很快就会看到此方法。目前你应该要知道此方法具有一些重要的功能,像是设定字段lstbox与elips的值为从XAML建立的ListBox和Ellipse element,以及为Button和ListBox控件设置事件处理函数。
CompileXamlWindow的构造函数没有设定窗口的Title property或其他任何内容,因为这些都是在XAML中处理的。但是它确实需要为list box填入数据。程序代码剩下的部分,是两个事件处理函数。ButtonOnClick处理函数就只是显示出MessageBox,你可能已经试过了。ListBox的SelectionChanged事件处理函数,会改变Ellipse对象的Fill property。虽然此事件处理函数从sender参数获得此ListBox对象,它其实也可以直接取用lstbox字段。你可以删除事件处理函数的第一个语句,程序的作用会一样。
当你编译并且执行此工程时,你会亲眼看到它生效了,这当然是相当重要的目标,但此时你可能也希望看一看它的工作原理。
看看工程目录下的obj子目录,可能是obj下面的Release或Debug子目录(取决于你是用何种方式编译的)。你会看到有一个文件名为CompileXamlWindow.baml。这个扩展名的意思是Binary XAML,发音为“bammel”。这是已经被解析、切割成token并且转成二进制格式的
XAML文件。此文件变成可执行程序的一部分,如同应用程序的资源(resource)一样。
你也会看到一个名为CompileXamlWindow.g.cs的文件,这是产生自XAML的文件(g表示generated)。将它用Notepad或别的文本查看器打开,这就是CompileXamlWindow类的另一部分,它会和CompileXamlWindow.cs文件被编译在一起。靠近类顶端的地方,你会看到lstbox和elips字段的声明。你也会看到InitializeComponent方法,在运行时加载BAML文件,并将它转成element tree。在此文件的底端,有方法会设定lstbox和elips字段,并设置事件处理函数。(有时候Visual Studio显示出的编译错误信息中,会有这些产生出来的文件。这时候你需要在不去编辑被产生文件的情况下解决错误。)
当CompileXamlWindow类的构造函数开始执行,窗口的Content property是null,而且所有的窗口property(例如Title、SizeToWindow与ResizeMode)都是默认值。在调用完InitializeComponent之后,Content是StackPanel,而且其他的property都被设定成XAML文件所指定的值。
只有在你将XAML和程序代码一起编译的情况下,CompileXamlWindow 程序所展示出来的XAML和C# 程序代码之间的关联方式(共享一个类、指定事件处理函数、设定字段),才有可能。当你直接(或间接)调用XamlReader.Load,在运行时加载XAML,你的选择就会比较少。你已经存取过XAML所建立的对象,但是要设定事件处理函数,或将此对象保存为字段,却不是容易的事。
关于XAML,常被问的一个问题是“我能在XAML中使用自己的类吗?”是的!你可以。为了让自定义类(定义在C# 文件)和整个工程一起编译,你只要在XAML文件中加上自定义类的名称声明即可。
假设你有一个自定义控件,名为MyControl,定义在C# 文件中,其CLR命名空间是MyNamespace。你将此C# 文件包含进此工程中,在XAML文件内,你必须先为此CLR命名空间建立一个前缀(prefix)的关联,比方说stuff,声明方式如下:
xmlns:stuff="clr-namespace:MyNamespace"
其中,“clr-namespace”必须是小写,后面要接着一个冒号。(这类似于常见XML声明中的http:部分,在下一章,我们会讨论更多引用外部动态链接库的语法。)此命名空间声明必须出现在第一次引用MyControl之前,或者作为MyControl元素的属性。此MyControl元素需要前置stuff:
你应该使用什么前缀?(我假设你已经拒绝用“stuff”作为一个一般目的的解决方案。)习惯上使用简短的前缀,但这不是硬性规定。如果你的项目包含来自多个CLR命名空间的源代码,你需要为每个命名空间都设定一个前缀,你可以让前缀类似CLR命名空间的名字,以避免混淆。如果所有的自定义类都在一个命名空间内,常常用src(意思是source code源代码)当作前缀。
让我们建立一个新的工程,名为UseCustomClass。此工程包含一个链接,连到第13章SelectColorFromGrid工程的ColorGridBox.cs文件。此ColorGridBox类的命名空间是Petzold.SelectColorFromGrid,所以为了要在XAML文件中使用此类,你需要下面的命名空间声明:
xmlns:src="clr-namespace:Petzold.SelectColorFromGrid"
下面是UseCustomClass.xaml文件,包含命名空间声明以便用src前缀引用ColorGridBox控件。
UseCustomClass.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:Petzold.SelectColorFromGrid"
x:Class="Petzold.UseCustomClass.UseCustomClass"
Title="Use Custom Class"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
VerticalAlignment="Center"
Margin="24"
SelectionChanged="ColorGridBoxOnSelectionChanged" />
Code-behind文件包含Main、对InitializeComponent的调用和ColorGridBox控件的SelectionChanged事件处理函数。
UseCustomClass.cs
//-----------------------------------------------
// UseCustomClass.cs (c) 2006 by Charles Petzold
//-----------------------------------------------
using Petzold.SelectColorFromGrid;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.UseCustomClass
{
public partial class UseCustomClass : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new UseCustomClass());
}
public UseCustomClass()
{
InitializeComponent();
}
void ColorGridBoxOnSelectionChanged(object sender,
SelectionChangedEventArgs args)
{
ColorGridBox clrbox = args.Source as ColorGridBox;
Background = (Brush) clrbox.SelectedValue;
}
}
}
UseCustomClass.cs文件需要将Petzold.SelectColorFromGrid命名空间加进来(利用using指示符),因为事件处理函数引用了ColorGridBox类。你可以改变该引用,改成只引用ListBox(ColorGridBox继承自ListBox),那么你就不需要使用此using指示符了。将SelectionChanged事件处理函数整个省略掉,是有可能的,可以在XAML中改用数据绑定,但是语法会有一点不一样,所以这部分等到第23章再来讨论。
稍早我提过,一般来说,每个窗口和对话框都会有一个XAML文件和一个对应的code-behind文件。但是不要因此误以为XAML文件不能用来代表Window以外的element。下面的UseCustomXamlClass工程定义了一个自定义类,继承自Button(虽然是很简单的类),完全用XAML来表示。使用XAML来定义自定义类的时候,是通过root element的x:Class attribute,这是此attribute唯一可以出现的地方。下面的XAML文件定义此Button派生类为CenteredButton。而XAML将HorizontalAlignment与VerticalAlignment property设定为Center,并且让按钮有一些边界(margin)。
CenteredButton.xaml
此XAML的命名空间是Petzold.IncludeApplicationDefinition,和MyWindow类是相同的命名空间。MyWindow继承自Window,一部分(partial)定义在MyWindow.cs中。此类的构造函数调用InitializeComponent,并且也包含Button的Click事件处理函数。
MyWindow.cs
//-----------------------------------------
// MyWindow.cs (c) 2006 by Charles Petzold
//-----------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Petzold.IncludeApplicationDefinition
{
public partial class MyWindow : Window
{
public MyWindow()
{
InitializeComponent();
}
void ButtonOnClick(object sender, RoutedEventArgs args)
{
Button btn = sender as Button;
MessageBox.Show("The button labled '" + btn.Content +
"' has been clicked.");
}
}
}
第二个XAML文件负责Application对象。命名空间依然是Petzold.Include ApplicationDefinition,但是类是MyApplication。
MyApplication.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Petzold.IncludeApplicationDefinition.MyApplication"
StartupUri="MyWindow.xaml" />
MyApplication.xaml文件的Build Action必须是ApplicationDefinition。小心:在某些情况下(例如更改文件名),Visual Studio可能会改变Build Action的设定。如果你使用XAML文件来定义一个Application对象,而你得到一个错误信息告诉你没有Main方法,那么请检查此Application XAML的Build Action设定。
请注意最后的StartupUri attribute,引用到MyWindow.xaml文件。当然,在应用程序运行的时候,MyWindow.xaml文件已经被编译成MyWindow.baml文件,成为应用程序的资源,但它依然是你希望应用程序一开始显示的窗口。这里的StartupUri attribute取代了Main里面调用的Run方法。
最后,下面是MyApplication.cs,它什么事都没做。在某些应用程序中,此文件可能具有Application对象需要的事件处理函数(如果MyApplication.xaml文件中的attribute定义了事件处理函数的话)。
MyApplication.cs
//----------------------------------------------
// MyApplication.cs (c) 2006 by Charles Petzold
//----------------------------------------------
using System;
using System.Windows;
namespace Petzold.IncludeApplicationDefinition
{
public partial class MyApplication : Application
{
}
}
整个工程已经全部介绍完毕。显然这里没有Main方法,但是在你编译程序之后,你可以看看产生出来的MyApplication.g.cs文件,你就可以看到Main。
MyApplication.cs文件是如此地没有意义,以至于你可以将它从工程中删除,此工程依然可以编译运行,一如往常。(事实上,当我第一次将这些文件组成工程时,我偶然地在MyApplication.xaml与MyApplication.cs中使用了不同的类名,结果依然顺利编译运行!)
只包含XAML文件,完全没有程序代码文件,这样的工程也是有可能的(虽然对许多应用程序来说并非如此)。没有程序代码的工程通常会在XAML中使用数据绑定,或使用某些XAML animation。下面的工程名为CompileXamlOnly,具有两个文件。第一个是Application文件:
XamlOnlyApp.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
StartupUri="XamlOnlyWindow.xaml" />
我稍早提到过,此Application文件必须将Build Action设定成ApplicationDefinition,否则一切就无法顺利运行。请注意,StartupUri是XamlOnlyWindow.xaml文件,内容如下:
XamlOnlyWindow.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Compile XAML Only"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
Height="100"
Margin="24"
Stroke="Red"
StrokeThickness="10" />
Height="100"
Margin="24">
Sunday
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
请注意,两个文件都没有定义类名称。如果你检查一下,你会发现Visual Studio只为Application XAML产生了文件,并将类命名为Application__。这个产生出来的文件包含了Main方法。对于Window XAML来说,没有产生文件。但是Visual Studio会将此Window编译成一个BAML文件,所以整个组织结构会类似于有明确程序代码的工程。对Application类来说,并不需要BAML文件,因为它没有定义一个element tree,或者定义运行过程中需要的任何东西。
假设你一开始有一个只有XAML的应用程序,然后你决定此应用程序需要一些C# 程序代码。但是你不想为此建立一个全新的C# 文件。我不知道你的原因是什么。(或许你会告诉我,这是你的项目,你高兴这么做。)幸好,你可以在XAML内嵌入C# 程序代码,这样做看起来不美观,但确实是可行的。
此工程名为EmbedCodeInXaml,第一个文件是Application类:
EmbeddedCodeApp.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
StartupUri="EmbeddedCodeWindow.xaml" />
此StartupUri引用到EmbeddedCodeWindow.xaml文件,此文件具有一个Button、一个Ellipse和一个ListBox,也具有一些内嵌的C# 程序代码。
EmbeddedCodeWindow.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Petzold.CompileXamlOnly.EmbeddedCodeWindow"
Title="Embed Code in XAML"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize"
Loaded="WindowOnLoaded">
Width="200"
Height="100"
Margin="24"
Stroke="Red"
StrokeThickness="10" />
Width="150"
Height="150"
Margin="24"
SelectionChanged="ListBoxOnSelection" />
void WindowOnLoaded(object sender, RoutedEventArgs args)
{
foreach (System.Reflection.PropertyInfo prop in
typeof(Brushes).GetProperties())
lstbox.Items.Add(prop.Name);
}
void ButtonOnClick(object sender, RoutedEventArgs args)
{
Button btn = sender as Button;
MessageBox.Show("The button labeled '" +
btn.Content +
"' has been clicked.");
}
void ListBoxOnSelection(object sender, SelectionChangedEventArgs args)
{
string strItem = lstbox.SelectedItem as string;
System.Reflection.PropertyInfo prop =
typeof(Brushes).GetProperty(strItem);
elips.Fill = (Brush)prop.GetValue(null, null);
}
]]>
嵌入的程序代码需要使用x:Code element以及x:Code element内的CDATA section。XML规范定义了CDATA(意思是“character data”)为XML文件内的一个section,作为“没有任何markup的文字内容”。对于C# 或其他语言,当然是属于CDATA。
CDATA section一定是以“”结束。在CDATA section内,绝对不可以出现“]]>”,因此,如果你写出下面的程序,就会有问题了:
if (array1[array2[i]]>5)
中间应该要插入一个空格,才不会被误判为CDATA的结尾。
编译此项目的时候,C# 程序代码被放进EmbeddedCodeWindow.g.cs文件。此嵌入式程序代码无法定义字段。如果产生出来的程序代码没有自动将这些命名空间用using指示符(directive)包含进来的话,此嵌入程序代码可能需要完整的命名空间。请注意,出现在EmbeddedCodeWindow.xaml的嵌入式程序代码,需要为类冠以完整的命名空间System. Reflection namespace。
虽然在XAML文件中嵌入C# 程序代码,好像很方便,但是却相当丑且不灵活。如果你不在XAML中嵌入C# 程序代码,你应该会过着更快乐、更长寿、更满足的生活。
或许你还记得第5章“Stack and Wrap”的DesignAButton程序。该程序指定一个StackPanel到Button的Content property,然后用两个Polyline对象、一个Label、一个Image(显示图BOOK06.ICO)来装饰StackPanel。让我们试着只用XAML(和此icon文件)模仿此程序。
此icon文件必须将Build Action设定成Resource。下面的DesignXamlButtonApp.xaml文件必须将Build Action设定成Application Definition。
DesignXamlButtonApp.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
StartupUri="DesignXamlButtonWindow.xaml" />
此工程最后的一部分,是Build Action被设定成Page的Window的XAML文件。
DesignXamlButtonWindow.xaml
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="Design XAML Button"
SizeToContent="WidthAndHeight"
ResizeMode="CanMinimize">
此窗口只包含一个Button,但是此Button包含一个StackPanel,而此StackPanel包含其他4个element。这两个Polyline element指定一系列(11个)XY坐标点。请注意,这里使用逗号分隔这些点。你可以改用空格来分隔点,用逗号来分隔XY坐标。或者你可以使用空白或者逗号同时作为两种分隔。
此XAML Image element也相当优雅。不再定义一个Uri对象,然后从此Uri建立一个BitmapImage,然后再将BitmapImage对象指定给Image的Source property(这一切都需要写在C# 程序代码中,你可以在原始的DesignAButton.cs文件中看到这一段程序代码)。现在你可以简单地将Source property设定成icon文件的名称。如果你想要的话,你可以引用此资源的URI(“pack://application:,,/BOOK06.ICO”),但是我认为文件名看起来清楚得多。
XAML包含许多像这样的小快捷方式,下一章开始,我们会陆续探讨它们。但是首先,我们需要一个程序,可以让我们互动地(interactively)进行XAML的实验,并且研究XAML的语法到极致。