当前位置: 代码迷 >> 综合 >> XAML(和Camel押韵) - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net
  详细解决方案

XAML(和Camel押韵) - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net

热度:37   发布时间:2024-01-18 17:34:09.0

导读:


本文转自
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的语法到极致。

  相关解决方案