阅读WPF揭秘前两章探索Silverlight运行的基本原理和RIA工作流程的密码(二)
2011-10-11 12:36 沐海 阅读(1214) 评论(0) 编辑 收藏 举报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对象:
这个情况下,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事件可以让我们知道处理何时完成。
有了这个Button的引用,就可以做任何想做的事:设置额外的属性(或许会使用一些难以用XAML表达的逻辑),添加事件处理程序,或者执行一些无法用XAML完成的动作,例如调用方法。
当然,使用硬编码的索引和其他关于用户界面结构假设的代码并不能让人满意,因为一旦稍微修改XAML它就无法工作了。相反,可以写一些代码来更通用地处理元素,并找到那个内容为“OK”字符串的Button元素,但是对于如此简单的任务来说,这样做就有些得不偿失了。另外,如果想让Button包含图形内容,该如何在多个按钮中识别出它呢?
幸运的是,XAML支持元素命名,这样就可以从过程式代码中找到这些元素并放心地使用它们了。
2.命名XAML元素
XAML语言命名空间有一个Name关键字,它是用来给元素命名的。如果是一个嵌入到窗口中的简单的OK Button,Name关键字可以这样使用:
有了以上代码后,就可以更改前面的C#代码了,可以使用Window的FindName方法来(递归地)搜索它的子元素,并返回想要的元素的实例。
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中的任何一个事件处理程序(通过事件特性,如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时构建系统生成了几个项,它们是:
·一个BAML文件(MyWindows.baml),作为默认的二进制资源它会被嵌入到程序集中。
·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[…]]>嵌入不是必须的,但是它至少避免了使用<作为;的替换符号以及&作为&的替换符号。这是因为虽然其他都是被当作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命名空间前缀 |
2.9 小结
你已经看到了XAML是如何与WPF配合的,最重要的是,你现在已经有足够的知识来把大多数的XAML实例转换为一种语言,如C#,也可以把一种语言转换为XAML。然而,因为类型转换器和标记扩展是“黑盒”,直接翻译或转换是不太可能的。也就是说,如果你不能理解在内部转换是如何进行的也没有关系,因为从过程式代码中直接调用类型转换器是可选的。(许多拥有对应的类型转换器的类会提供一个静态的Parse方法,做的是同样的事情,这是为了简化过程式代码才加进去的。)
这些本应该在XAML中特别处理的简单概念(如null),使用了第三方用的相同的标记扩展机制来表示,所以我很喜欢。它将XAML语言变得尽可能地简单,并保证了可扩展机制可以很好地运作。
进一步了解WPF之后,你可能会发现,一些WPF API在过程式代码中调用有些不方便,因为它们的设计经常为XAML使用而优化的。例如,WPF提供了许多小型构建块[在第1章中讲到,这些构建块可以帮助你完成富创作(rich composition)],因此WPF应用程序通常必须比像Windows Forms这样的应用程序手工创建更多的对象。除了XAML在简练表现对象的深度层次方面更加出色以外,WPF团队花了很多时间来实现在XAML中高效隐藏过渡对象(如类型转换器)的功能,而不是在过程式代码中隐藏它们(例如,创建内部对象的构造函数)。
大多数人理解WPF由XAML提供单独的声明型换型的好处,但有些人也为把XML作为所选格式而痛惜。下面有两段常见的抱怨,我来说说。
2.9.1 抱怨1:XML太过冗长不便于输入
确实如此!几乎没有人喜欢输入很多XML,但其实有许多工具。附录中列出了一些可以减少大量输入的工具。可以基于XML Schema定义(XSD)以自动完成拼写的形式完成,还有一些可视化设计器不需要你一个个输入尖括号。XML的透明性和明确性使得开发过程中引入新工具变得很简单(例如,你可以为最喜欢的工具创建一个XAML导出器),也使定制或者故障排除变得简便。
在WPF的某些领域,如遇有复杂的路径和形状、3D模型等,手工输入XAML是不现实的。事实上,当XAML第一次在beta版中被引入的时候,这种将一些人工输入的快捷方式移除的趋势已经形成,这样有利于促进格式的可预测性和可扩展性,从而得到更好的工具支持。但是我仍然相信通过熟透XAML,从过程化和声明式的观点看WPF API是学习这项技术的最好方式,这就像在不依靠FrontPage这样的工具的情况下理解HTML一样。
2.9.2 抱怨2:基于XML的系统性能差
XML是改进互操作性的格式,而不是用于数据高效呈现的。那么,为什么大多数WPF应用程序可以承受巨大而又解析较慢的数据呢?
好在一般的WPF方案中,XAML是被编译为BAML的,因此不需要对大小和运行时的解析性能付很高的代价。BAML比原来的XAML文件要小,并对运行时的性能做了优化。因此,XML的性能缺陷通常只会影响开发时,而此时又是最需要XML的好处的。
第3章 WPF的重要新概念
本章内容
·逻辑树与可视树
·依赖属性
·路由事件
·命令
·漫游类层次
我们即将完成本书的第一部分,在开始一些真正有趣的话题之前,回顾一下之前介绍的一些主要概念是很有用的,这也是.NET程序员们所不熟悉的部分。本章中的一些主题是WPF陡峭学习曲线中最主要的东西。熟悉了这些概念,你将能够很自信地学习本书的剩余部分(或其他任何WPF文档)。
本章中的一部分概念是全新的(例如逻辑与可视树),而另一部分是那些常见概念的扩展(如属性和事件)。通过学习每一个概念时,你将看到如何将它们应用到一个简单的用户界面——About对话框上,很多程序都需要这个界面。
3.1 逻辑树与可视树
XAML天生就是用来呈现用户界面的,这是由于它具有层次化的特性。在WPF中,用户界面由一个对象树构建而成,这棵树叫作逻辑树。
代码清单3-1定义了假想的About对话框的雏形,使用Window作为逻辑树的根节点。Window拥有一个StackPanel子元素(将在第6章讲解),它包括一些简单的控件和另一个StackPanel,而这个StackPanel中还有一些Button控件。
代码清单3-1 以XAML形式表示的一个简单的About对话框
图3-1展示了呈现出来的对话框(通过把代码清单3-1的内容粘贴到像XamlPad这样的工具中,很容易做出一个对话框),图3-2展示了这个对话框的逻辑树。
图3-1 根据代码清单3-1呈现的对话框 |
图3-2 代码清单3-1的逻辑树 |
注意,即使是WPF用户界面的逻辑树也并不是用XAML创建。代码清单3-1完全可以用过程式代码来实现,而逻辑树是一致的。
逻辑树的概念很直观,但是为什么要关注它呢?因为几乎WPF的每一方面(属性、事件、资源等)都有与逻辑树相关联的行为。例如,属性值有时会沿着树自动传递给子元素,而触发的事件可以自底向上或自顶向下遍历树,这两种行为将在本章后面讲解。
与逻辑树类似的一个概念是可视树。可视树基本上是逻辑树的扩展,在可视树中,节点都被打散,分放到核心可视组件中。可视树提供了一些详细的可视化实现,而不是把每个元素当作一个“黑盒”。例如,虽然ListBox从逻辑上讲是一个单独的控件,但它的默认可视呈现是由更多的原始WPF元素组成的:一个Border对象、两个ScrollBar及其他一些元素。
并非所有的逻辑树节点都会出现在可视树中,只有从System.Windows.Media.Visual或System.Windows.Media.Visual3D派生的元素才会被包含进去。其他元素(和一些简单的字符串内容,如代码清单3-1中的内容)不会包含在内,因为它们自己并没有与生俱来的呈现行为。
图3-3是代码清单3-1在Windows Vista中使用Aero主题运行后生成的默认可视树,该图表提供了一些内部的不可见UI组件,例如ListBox的两个ScrollBar元素和每个Label的Border元素。这也告诉我们,Button、Label和ListBoxItem这3个元素,除了Button使用了一个模糊的ButtonChrome元素而不是Border元素外,其他的组成元素都是相同的。(由于这些控件有不同的默认属性值,它们会有一些其他的外观差异,例如,Button有一个默认的边界宽度Margin为10,这是在上下左右四个方向上都有的,而Label的默认Margin是0。)
图3-3 代码清单3-1的可视树,突出了逻辑树节点 |
提示 在XamlPad的工具栏中,有一个按钮可以显示出任何一个呈现出来的XAML的可视树(和属性值)。当XAML放入一个Window对象时(如图3-1中的情况),无法看到它的可视树,但只要将该Window元素改为Page元素(并删除SizeToContent属性),就能享受这一功能所带来的好处。
由于这一功能可以让你从深层次观察WPF元素的组成,所以可视树可能变得相当复杂。幸运的是,虽然可视树是WPF架构的核心组成部分,但通常不用去管它们,除非你正对控件进行完全的重塑(在第10章中讲解)或者做一些底层绘制(在11.1节中讲解)。根据一个按钮的可视树来写代码,例如,你想破坏WPF的核心原则之一——可视部分和逻辑部分分开。当使用第10章中的技术来改变像Button这样的控件风格时,整个可视树会被一些截然不同的东西所取代。
因此,使用System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper这两个有些对称的类可以方便地遍历逻辑树和可视树。代码清单3-2包含了代码清单3-1的一个代码隐藏文件,当它在调试器下运行时,输出的是基于深度优先的About对话框的逻辑树和可视树。(还要向代码清单3-1中添加x:Class="AboutDialog"及对应的xmlns:x标签,以便把它与过程式代码关联起来。)
注意 不要根据具体的可视树写代码!
逻辑树是静态的,不会受到程序员的干扰(例如动态添加/删除元素),但只要用户切换不同的Windows主题,可视树就会改变。
代码清单3-2 遍历和打印逻辑树和可视树
当使用depth=0和当前的Window实例调用这些方法时,其结果就是一个基于文本的树,其中的节点与图3-2和图3-3中的完全相同。虽然在Window的构造函数中就可以遍历逻辑树,但可视树直到Window完成至少一次布局之后才会有节点,否则是空的。这也是为什么PrintVisualTree是在OnContentRendered中调用的,因为OnContentRendered是在布局完成之后才被调用的。
有些时候,可以用元素自己的实例方法在两种树中进行操作。例如,Visual类包含了3个protected的成员(VisualParent、VisualChildrenCount和GetVisualChild)用于验证它的可视父节点和孩子节点。FrameworkElement是一个通用的控件基类(例如Button和Label就是从FrameworkElement派生而来),它有一个公共的Parent属性用于呈现逻辑父节点。某些FrameworkElement的子类会以不同的方式提供它们的逻辑子元素。例如,有些类提供了Children集合,其他类(如Button和Label)则提供了Content属性,这表示元素只能够有一个逻辑子节点。
提示 像图3-3中呈现的可视树通常会被简化为元素树,因为它们既包含了逻辑树中的元素,也包含了可视树中的某些元素。术语“可视树”是用来描述任何一棵包含可视元素(这些元素不是逻辑元素)的子树。例如,大多数人会说Window元素的默认可视树是由一个Border、一个AdornerDecorator、两个AdornerLayer和一个ContentPresenter组成的。在图3-3中,最上层的StackPanel是不作为ContentPresenter的可视子元素的,但事实是,VisualTreeHelper会把StackPanel作为一个可视子元素。
3.2 依赖属性
WPF引入了一个新的属性类型叫作依赖属性,整个WPF平台中都会使用到它,用来实现样式化、自动数据绑定、动画等。你可能在怀疑论(skepticism)中第一次遇到这个概念,它使得.NET类型图变得很复杂,其中有简单的字段、属性、方法和事件。但是在你理解依赖属性解决的问题之后,很有可能会把它们作为一种“不错的添加剂”。
依赖属性在任何时刻都是依靠多个提供程序来判断它的值的。这些提供程序可以是一段一直在改变值的动画,或者一个父元素的属性值从上慢慢传递给子元素等。依赖属性的最大特征是其内建的传递变更通知(change notification)的能力。
添加这样的智能给属性,其动力在于能够声明标记中直接启用富功能(rich functionality)。WPF友好声明设计的关键在于它使用了很多属性。例如,Button控件有96个公共属性!属性可以方便地在XAML中设置(直接或者通过设计工具)而不用程序代码。但是如果依赖属性没有额外的垂直传递,在不写额外代码的情况下,很难在设置属性这样简单的动作中获得想要的结果。
在本节中,我们将简要地看一下依赖属性的实现,让讨论更加具体。然后我们再深入分析依赖属性在普通.NET属性上赋值的下面一些方式:
·变更通知
·属性值继承
·对多提供程序的支持
理解大多数依赖属性的细微差别,通常只是对于自定义控件设计者来说是重要的。然而,即使是WPF的普通用户,最终也需要了解依赖属性是什么以及它们如何工作。例如,你只能为依赖属性添加风格和动画效果。在使用WPF工作一段时间之后,你会发现你其实希望所有的属性都是依赖属性!
3.2.1 依赖属性的实现
实际上,依赖属性仅仅是普通的.NET属性,只不过它已融入到了WPF架构中。它完全是由WPF API实现的,没有一种.NET语言(除了XAML以外)天生就能理解依赖属性。
代码清单3-3展示了一个Button如何有效地实现一个叫作IsDefault的依赖属性。
代码清单3-3 一个标准的依赖属性实现
IsDefaultProperty静态成员是真正的依赖属性,类型为System.Windows.DependencyProperty。按规则,所有的DependencyProperty成员都必须是public、static,并且有一个Property作为后缀。依赖属性通常是通过调用DependencyProperty.Register静态方法创建的,这样的方法需要一个名称(IsDefault)、一个属性类型(bool)以及拥有这个属性的类(Button类)。通过不同的Register方法重载,你可以传入metadata(元数据)来告诉WPF如何处理该属性、如何处理属性值改变的回调、如何处理强制值转换,以及如何验证值。Button会在它的静态构造函数中调用Register的重载,给依赖属性一个默认值false,并为变更通知添加一个委托。
最后,那个叫作IsDefault的传统.NET属性会调用继承自System.Windows.Dependency- Object的GetValue和SetValue方法来实现自己的访问器,System.Windows.DependencyObject是底层基类,这是拥有依赖属性的类必须继承的。GetValue返回最后一次由SetValue设置的值,如果SetValue从未被调用过,那么就是该属性注册时的默认值。IsDefault .NET属性(有时叫作此上下文中的属性包装器)并不是必需的,Button的使用者可能会直接调用GetValue/ SetValue方法,因为它们是公开的。但是.NET属性会让以编程方式读写属性变得更加自然,它还允许通过XAML设置属性。
注意 在运行时,绕过了.NET属性包装器在XAML中设置依赖属性。
虽然XAML编译器在编译时是依靠该属性包装器的,但在运行时WPF是直接调用GetValue和SetValue的!因此,为了让使用XAML设置属性与使用过程式代码设置属性保持一致,在属性包装器中除了GetValue/SetValue调用以外,不应该包含任何其他逻辑,这是至关重要的。如果需要添加自定义逻辑,应该在注册的回调函数中添加。所有WPF的内建属性包装器都应遵守这个规则,因此这个警告是针对那些打算写带有依赖属性的自定义类的人的。
从表面上看,代码清单3-3像是一种冗长的呈现简单布尔属性的方式。然而,因为GetValue和SetValue内部使用了高效的稀疏存储系统,而IsDefaultProperty是一个静态成员(而不是一个实例成员),与典型的.NET属性相比,依赖属性的实现节省了保存每个实例所需要的内存。如果WPF控件的所有属性都是实例成员的包装器(与大部分.NET属性一样),由于所有的本地数据都会被添加到每个实例中,这样将会消耗大量的内存。每个Button有96个成员,每个Label有89个成员……,它们消耗的内存增长起来会很快!实际上,Button的96个属性中有78个是依赖属性,而Label的89个属性中的71个是依赖属性。
然而,依赖属性的好处远远不止内存使用这一项。它把相当一部分代码集中起来,并做标准化处理,这部分代码原本是要由属性实现者自己来写的,用来检查线程访问、请求容器元素重新呈现等。例如,当属性的值改变时(如Button的Background属性),如果它要求子元素重新呈现,就只要传递一个FrameworkPropertyMetadataOptions.AffectsRender标志给DependencyProperty. Register的一个重载。这一实现也列出了之前提到的3个特征,现在我们一个一个来讲解,首先是变更通知。
3.2.2 变更通知
无论何时,只要依赖属性的值改变了,WPF就会自动根据属性的元数据(metadata)触发一系列动作。这些动作可以重新呈现适当的元素、更新当前布局、刷新数据绑定等。内建的变更通知最有趣的特性之一是属性触发器,它可以在属性值改变时执行自定义动作,而不用更改任何过程式代码。
例如,假设你想让代码清单3-1中About对话框的每个Button的文本,在鼠标指针移上去时变为蓝色。如果没有属性触发器的话,你得为每个Button添加两个事件处理程序,一个是为MouseEvent事件准备的,一个是为MouseLeave事件准备的。
下面的C# code-behind文件中实现了这两个事件处理程序。
然而,有了属性触发器,你可以完全在XAML中完成相同的行为。下面Trigger对象就是需要写的所有代码:
这个触发器能够基于Button的IsMouseOver属性工作,当MouseEnter触发时,IsMouseOver属性会变为true;在MouseLeave触发时,它又变为false。注意,当IsMouseOver变为false时,不用把Foregound变为黑色,这是WPF自动完成的!
唯一需要做的就是把这个Trigger赋给每个Button。不幸的是,因为WPF 3.0的人为限制,不能把属性触发器直接应用到Button这样的元素上。它们只能在Style对象内部应用,因此在第10章中,我们将深入讲解属性触发器。同时,如果需要验证一下属性触发器,可以把触发器放入下面的过渡性XML元素,来把前面的Trigger应用到这个Button上。
然而,属性触发器仅仅是WPF支持的3种触发器之一。数据触发器是属性触发器的另一种形式,它可以在任何.NET属性中工作(而不仅仅是依赖属性),数据触发器将在第10章中讲解。事件触发器会通过声明方式指定动作,该动作在路由事件(将在本章中讲解)触发时生效。由于事件触发器总是与动画或声音一起工作,所以将在第13章中讲解。
注意 不要被元素的Triggers集合所愚弄!
FrameworkElement的Triggers属性是一个可读写的TriggerBase项(三种触发器类型的通用基类)集合,因此它是把属性触发器添加到item控件(如Button)的一种简单方式。不幸的是,这个集合只能包含WPF 3.0的事件触发器,因为WPF团队没有时间来实现这种支持。如果你尝试添加一个属性触发器(或数据触发器)到集合中,将导致运行时抛出一个异常。
3.2.3 属性值继承
术语“属性值继承”(简称属性继承)并不是指传统的面向对象的类继承,而是指属性值自顶向下沿着元素树传递。代码清单3-4展示了属性继承的简单示例,在代码清单3-1的基础上作了更新,显式地设置FontSize和FontStyle这两个依赖属性。图3-4展示了改变后的结果。(注意,是简单的SizeToContent设置,实现了窗口自动改变大小适应所有内容的功能!)
图3-4 根Window元素上设置了FontSize和FontSytle的About对话框 |
代码清单3-4 根Window元素上设置了Font属性的About对话框
对于大部分元素来说,这两个设置将会沿着逻辑树向下传递,并由子元素继承。这甚至会影响Button和ListBoxItem元素,尽管它们位于逻辑树的第三层。第一个Label的FontSize并没有改变,因为它被显式地设置为20,重载了继承的值30。而继承后的FontStyle的Italic(斜体)设置将会影响所有的Label、ListBoxItem和Button元素,这是因为它们没有被显式设置过。
注意,虽然StatusBar也像其他控件一样支持这两个属性,但它的文本并没有受到任何值的影响。属性值的继承行为是由以下两种因素决定的:
·并不是每个依赖属性都参与属性值继承的。(从其内部来讲,依赖属性会通过传递FrameworkPropertyMetadataOptions.Inherits给DependencyProperty.Register方法来完成继承。)
·有其他一些优先级更高的源来设置这些属性值,下一节中会解释这个内容。
在本例中,满足了后面一种因素。有一些控件如StatusBar、Menu和ToolTip控件,其内部会把字体属性设置为当前的系统设置。通过这种方式,用户可以在控制面板中控制它们的字体,这对于用户来说很熟悉。但结果可能会让你觉得迷惑,因为这样的控件最终会“吞噬”所有的继承,它会阻止继承继续沿着元素树向下传递。例如,如果把一个Button作为一个逻辑子元素添加到StatusBar控件中(见代码清单3-4),它的FontSize和FontStyle的默认值分别是12和Normal,这与StatusBar之外的其他按钮的值是不同的。
在其他地方使用属性值继承
属性值继承最初是基于元素树设计的,但它已经被扩展到了其他一些上下文中。例如,值可以被传递给一些看起来像XML子元素一样的元素(这是由XAML属性元素语法造成的),但是它们并不是逻辑树或者可视树中的子元素。这些伪子元素(pseudo-children)可以是一个元素的触发器或者任何属性的值(并不仅仅是Content或者Children属性),只要它是从Freezable派生而来的对象就行。这听上去可能有点霸道,且没有文档说明这一点,但这么做的目的是让几个基于XAML的方案能够按照你的意愿工作,而不用想太多。
3.2.4 对多个提供程序的支持
WPF有许多强大的机制可以独立地去尝试设置依赖属性的值。如果没有设计良好的机制来处理这些完全不同的属性值提供程序,这个系统会变得混乱,属性值会变得不稳定。当然,正如它们的名字所表达的,依赖属性就是设计为以一致的、有序的方式依靠这些提供程序。
图3-5展示了这5步流程,通过该流程,WPF运行每个依赖属性并最终计算出它的值。多亏了依赖属性中内嵌的变更通知,这个流程才可以自动发生。
图3-5 计算依赖属性值的管道 |
1.第一步:判断基础值
大多数属性值提供程序会把基础值的计算纳入考虑范畴。下面的代码清单显示了8个提供程序,它们可以设置大多数依赖属性的值,优先级顺序从高到低为:
(1) 本地值 (5) 主题样式触发器
(2) 样式触发器 (6) 主题样式设置程序
(3) 模板触发器 (7) 属性值继承
(4) 样式设置程序 (8) 默认值
你已见到了一些属性值提供程序,如属性值继承。本地值,技术上的含义是任何对DependencyObject.SetValue的调用,但是它通常会有一个简单的属性赋值,这是用XAML或者过程式代码完成的(由依赖属性的实现方式造成,在之前的Button.IsDefault示例中已经看到过了)。默认值指的是依赖属性注册时使用的初始值,自然其优先级是最低的。其他样式和模板方面的提供程序将在第10章中讲解。
这个优先级顺序解释了为什么代码清单3-4中的StatusBar控件的FontSize和FontStyle属性没有受到属性值继承特性的影响。StatusBar字体属性的设置是与系统设置一致的,这是由主题样式设置程序(上面清单中的第6项)负责实现的。
虽然这比属性值继承(上面清单中的第7项)的优先级要高,但是仍然可以使用更高优先级的机制来重载这些字体设置,例如设置StatusBar的本地值。
2.第二步:计算
如果第一步中的值是表达式(派生自System.Windows.Expression的一个对象),那么WPF会执行一种特殊的演算步骤——把表达式转换为具体的结果。在WPF 3.0中,表达式仅在使用动态资源(在第8章中有讲解)或数据绑定(第9章中的话题)时起作用。在WPF的未来版本中,可能会允许使用其他类型的表达式。
3.第三步:应用动画
如果一个或者多个动画在运行,它们有能力改变当前的属性值(使用第二步计算出来的值作为输入)或者完全替代当前的属性值。因此,动画(第13章的话题)胜过其他任何属性值提供程序——就连本地值也不是它的“对手”!这经常是一些初学WPF的人容易感到困惑的地方。
4.第四步:限制(Coerce)
在所有属性值提供程序处理过之后,WPF将拿到一个几乎是终值的属性值,如果依赖属性已经注册了CoerceValueCallback,还会把这个属性值传递给CoerceValueCallback委托。该回调函数负责返回一个新的值,它是基于自定义逻辑实现的。例如,内建的WPF控件,像ProgressBar,使用这个回调来限制一个叫作Value的依赖属性,这个值大于Minimum(最小值)常数,小于Maximum(最大值)常数,如果输入值小于Minimum,则返回Minimum;如果输入值大于Maximum,则返回Maximum。
5.第五步:验证
最后,如果依赖属性已经注册了ValidateValueCallback,之前的限制中的值将被传入ValidateValueCallback委托。如果输入值有效,该回调函数必须返回true;否则就返回false。返回false将会导致抛出一个异常,并使整个流程被取消。
提示 如果没办法判断依赖属性从哪里获得当前的值,那么可以使用静态方法Dependency- PropertyHelper.GetValueSource作为调试助手。该方法将返回一个ValueSource结构,其中包括了以下一些数据:一个BaseValueSource枚举值,它反映的是基础值从哪里来的(流程中的第一步);IsExpression、IsAnimated和IsCoerced几个布尔类型属性,它反映了第二步到第四步的信息。
当在代码清单3-1中调用StatusBar实例中的这个方法,或者在代码清单3-4中使用FontSize或FontStyle属性,返回的BaseValueSource是DefaultStyle,表示这个值是从一个主题样式设置程序中来的。(主题样式有时也叫作默认样式,它的触发器的枚举值是DefaultStyleTrigger。)
请不要在程序代码中使用这个方法!WPF以后的版本中将打破值计算的假设,会根据它的源类型采用不同的方式处理属性值,而不是根据假设WPF应用程序中的方式来处理。
清除本地值(Local Value)
之前的3.2.2节中演示了如何使用过程式代码来把Button的Foreground(前景色)变为蓝色,来响应MouseEnter事件,然后再把它变回黑色去响应MouseLeave事件。这种方式的问题在于MouseLeave中本地值被设置为黑色,这与Button的初始状态是很不同的,因为初始状态中黑色是通过主题样式中的一个设置程序来完成设置的。如果主题改变,新主题将尝试改变Foreground的默认颜色(如果具有更高优先级的提供程序想做同样的事),本地设置将为黑色。
你很可能需要清除本地值,并让WPF从下一个最高优先级的提供程序中获得值,然后使用这个值来设置最终的属性值。幸运的是,DependencyObject确实提供了这种机制,可以通过调用ClearValue方法来实现。在下面的C#代码中,调用了Button b的ClearValue方法:
(Button.ForegroundProperty是一个DependencyProperty静态成员),在调用ClearValue之后,会重新计算基础值,并把本地值从方程式中删除。
注意,在3.2.2节中IsMouseOver属性的触发器在实现事件处理程序时,没有出现相同的问题。一个触发器可以处于激活状态也可以处于未激活状态,当处于未激活状态时,WPF在属性值计算中会忽略该触发器。
3.2.5 附加属性
附加属性是依赖属性的一种特殊形式,可以被有效地添加到任何对象中。一开始,这可能听上去很奇怪,但是这个机制在WPF中有多种应用。
拿About对话框的例子来说,想象如果不设置整个Windows元素的FontSize和FontStyle(在代码清单3-4中),而是在内部的StackPanel上设置它们的话,这两个属性仅仅会被两个Button继承。然而,把属性特性移到内部的StackPanel元素中没有什么作用,因为StackPanel自己没有任何与字体相关的属性。相反,你必须使用FontSize和FontStyle附加属性,这是在一个叫作TextElement的类中定义的。代码清单3-5演示了如何使用这两个附加属性,其中还介绍了一种新的XAML语法,这是专门为附加属性设计的。这样就能启用一些我们所盼望的属性值继承特性,如图3-6所示。
代码清单3-5 About对话框,把Font属性移到了内部的StackPanel中 TextElement.FontSize和TextElement.FontStyle(而不是简单的FontSize和FontStyle属性)必须在StackPanel元素中使用,因为StackPanel没有这两个属性。当XAML解析器或者编译器遇到这种语法时,它就要求TextElement(有时叫作附加属性提供者)有两个静态方法分别叫作SetFontSize和SetFontStyle,这样它们才可以设置相应的属性值。代码清单3-5中列出的StackPanel的声明与下面的C#代码是一样的: 注意,像FontStyles.Italic、Orientation.Horizontal和HorizontalAlignment.Center这样的枚举值,在之前的XAML中被简单地写成了Italic、Horizontal和Center。之所以可以这么做是因为有.NET Framework中的EnumConverter类型转换器,它可以转换所有不区分大小写的字符串。 虽然代码清单3-5中的XAML很好地把FontSize和FontStyle作为StackPanel的逻辑附加属性,但是C#代码告诉我们这里并没有什么神奇之处,仅仅是调用了与一个元素相关联的方法,而那个元素中有一个与其他东西没有关系的属性。说到附加属性,有趣的事情之一就是.NET属性都不是附加属性! 从内部看,SetFontSize这样的方法会只是调用DependencyObject.SetValue方法,通常每一个普通的依赖属性访问器都会调用同一个方法,但是这里调用的是传进来的DependencyObject上的DependencyObject.SetValue方法,而不是当前实例上的: 与此类似,附加属性也定义了一个静态方法叫作GetXXX(XXX是属性的名称),它会调用我们所熟悉的DependencyObject.GetValue方法: 与普通依赖属性的属性包装器一样,这些GetXXX和SetXXX方法只能调用GetValue和SetValue方法,不能挪作他用。 理解附加属性提供程序 代码清单3-5中所使用的FontSize和FontStyle附加属性,最令人困惑的部分是,它们并不是在Button或者Control中定义的,而这些却是定义普通FontSize和FontStyle依赖属性的基类。它们是看似不相关的TextElement类定义的(还有在TextBlock类中定义,其实在前面的示例中也可以使用TextBlock类)。 既然TextElement.FontSizeProperty是一个与Control.FontSizeProperty无关的Depen- dencyProperty成员(而TextElement.FontStyleProperty也是与Control.FontStyleProperty无关的),那么这一切是如何实现的呢?关键在于这些依赖属性在内部注册的方式。如果你看看TextElement的源代码,就会发现类似下面的东西: 这与之前那个注册Button的IsDefault依赖属性的示例很相似,但这里RegisterAttached方法对附加属性的属性元数据(property metadata)的处理作了优化。 从另一方面讲,Control不能注册FontSize依赖属性!它需要调用TextElement已经注册的属性的AddOwner方法,获得对同一个实例的引用: 因此,所有控件继承的FontSize、FontStyle属性和其他与字体相关的依赖属性都是由TextElement提供的属性! 幸运的是,在大多数情况下,提供附加属性(如GetXXX和SetXXX方法)的类就是定义一些通用依赖属性的类,从而避免了混淆。 虽然About对话框示例为了实现高级属性值的继承使用了附加属性,但附加属性通常都是用于用户界面元素的布局。(事实上,附加属性一开始是为WPF的布局系统而设计的。)派生自Panel的各种类定义了一些附加属性,用来把它们添加到子元素上来控制它们的摆放。通过这种方式,每个Panel可以把自定义行为给任何一个子元素,而不需要所有的子元素都具有自己的相关属性集。这种方式也让像布局这样的系统扩展起来更加简单方便,因为任何人都可以写一个带有自定义附加属性的新Panel控件。这在第6章和第17章中将有详细讲解。 把附加属性作为一种扩展机制 类似于以往Windows Forms那样的技术,许多WPF类定义了一个Tag属性(类型是System.Object),目的是为了存储每一个实例的自定义数据。但是要添加自定义数据给任何一个派生自DependencyObject的对象,附加属性是一种更加强大、更加灵活的机制。通常我们会忽略一点,即你可以用附加属性高效地向密封类(sealed class)的实例添加自定义数据(WPF中到处都是密封类)。 另外,大家对附加属性有一个曲解,虽然在XAML中设置它们依赖于SetXXX静态方法,但是可以在过程式代码中绕过这个方法,直接去调用DependencyObject.SetValue方法。这意味着在过程式代码中,可以把任何一个依赖属性作为一个附加属性。例如,下面这行代码把ListBox的IsTextSearchEnabled属性添加到了Button控件上,并赋予该属性一个值: 虽然这似乎没有任何意义,也不会给这个Button带来一些神奇的新功能,但是你可以用一种对应用程序或者组件有意义的方式来随意使用这个属性值。 这种方法还有很多有趣的方式可以用来扩展元素。例如,FrameworkElement的Tag属性是一个依赖属性,因此可以把一个GeometryModel3D(在12.1节中将会再次见到这个类,它是一个密封类,没有Tag属性)的对象实例附加给它。 这仅仅是WPF提供可扩展性的一种方式之一,但不需要用到传统的继承特性。 |
3.3 路由事件
正如WPF在简单的.NET属性概念上添加了许多基础的东西一样,它也为.NET事件添加了许多基础的东西。路由事件是专门设计用于在元素树中使用的事件。当路由事件触发后,它可以向上或向下遍历可视树和逻辑树,用一种简单而且持久的方式在每个元素上触发,而不需要使用任何定制代码。
事件路由让许多应用程序不去留意可视树的细节(对于样式重置来说这是很不错的),并且对于成功的WPF元素创作至关重要。例如,Button有一个Click事件,这是基于底层的MouseLeftButtonDown事件或者KeyDown事件实现的。当用户的鼠标指针位于标准按钮之上,且按下鼠标左键的时候,它们实际上是与ButtonChrome或者TextBlock可视子元素在交互。由于事件遍历了可视树,所以Button元素最终会发现这个事件,并处理该事件。类似地,在前面一章中,对于VCR样式的Stop(停止)按钮来说,一个用户可能在Rectangle逻辑子元素上直接按下鼠标左键。由于事件遍历了逻辑树,Button元素还是会发现这个事件,并处理该事件。(如果你真的希望能够区分这个事件是Rectangle上的还是凸出的Button上的,也可以自己去区分代码。)
因此,你可以在一个元素(如Button)中嵌入任何复杂内容或者(使用第10章中的技术)设置一棵复杂的可视树,鼠标左键单击其中任何一个内部元素,仍然会触发父元素Button的Click事件。如果没有路由事件,内部内容的创造者或者按钮的使用者不得不编写代码来把事件串起来。
路由事件的实现和行为与依赖属性有许多相同的地方。与之前对依赖属性的探讨一样,我们将先来看看一个简单的路由事件具体是如何实现的,然后将讲解路由事件的一些特性,并把这些特性应用到About对话框中。
3.3.1 路由事件的实现
在大多数情况下,路由事件与.NET事件看上去比较相似。与依赖属性一样,没有一种.NET语言(除了XAML以外)天生具有理解路由指派的能力。这些支持其实是基于WPF API的。
代码清单3-6演示了Button是如何有效地实现Click路由事件的(Click实际上是由Button的基类实现的,但这并不重要)。
就像依赖属性是由公共的静态DependencyProperty成员加上一个约定的Property后缀名构成的一样,路由事件也是由公共的静态RoutedEvent成员加上一个约定的Event后缀名构成的。路由事件的注册很像在静态构建器中注册依赖属性,它会定义一个普通的.NET事件或者一个事件包装器(event wrapper),这样可以保证在过程式代码中使用起来更加熟悉,并且可以在XAML中用事件特性语法(event attribute syntax)添加一个事件处理程序。与属性包装器一样,事件包装器在访问器中只能调用AddHandler和RemoveHandler,而不应该做其他事情。
代码清单3-6 一个标准的路由事件的实现
这些AddHandler和RemoveHandler方法没有从DependencyObject继承,而是从System. Windows.UIElement继承的,UIElement是一个更高层的供元素(如Button元素)继承的基类(该类层次将在本章最后进一步讲解)。这些方法可以向一个适当的路由事件添加一个委托或者从路由事件移除一个委托。在OnMouseLeftButtonDown中,它使用适当的RoutedEvent成员调用RaiseEvent(也是在UIElement基类中定义的)来触发Click事件。当前的Button实例(this)被传递给事件的源元素(source element)。在代码清单中没有列出,但是作为对KeyDown事件的响应,Button的Click事件将被触发,这样就可以处理由空格键或回车键完成点击动作的情况。
3.3.2 路由策略和事件处理程序
当注册完毕后,每个路由事件将选择3个路由策略中的一个。所谓路由策略就是事件触发遍历整棵元素树的方式,这些策略由RoutingStrategy枚举值提供。
·Tunneling(管道传递)——事件首先在根元素上被触发,然后从每一个元素向下沿着树传递,直到到达源元素为止(或者直到处理程序把事件标记为已处理为止)。
·Bubbling(冒泡)——事件首先在源元素上被触发,然后从每一个元素向上沿着树传递,直到到达根元素为止(或者直到处理程序把事件标记为已处理为止)。
·Direct(直接)——事件仅在源元素上触发。这与普通.NET事件的行为相同,不同的是这样的事件仍然会参与一些路由事件的特定机制,如事件触发器。
路由事件的事件处理程序有一个签名,它与通用.NET事件处理程序的模式匹配:第一个参数是一个System.Object对象,名为sender,第二个参数(一般命名为e)是一个派生自System. EventArgs的类。传递给事件处理程序的sender参数就是该处理程序被添加到的元素。参数e是RoutedEventArgs的一个实例(或者派生自RoutedEventArgs),RoutedEventArgs是EventArgs的一个子类,它提供了4个有用的属性:
·Source——逻辑树中一开始触发该事件的元素。
·OriginalSource——可视树中一开始触发该事件的元素(例如,TextBlock或者标准Button元素的ButtonChrome子元素)。
·Handled——布尔值,设置为true表示标记事件为已处理,这就是用于停止Tunneling或Bubbling的标记。
·RoutedEvent——真正的路由事件对象(如Button.ClickEvent),当一个事件处理程序同时被用于多个路由事件时,它可以有效地识别被触发的事件。
Source和OriginalSource的存在允许使用更高级别的逻辑树或者更低级别的可视树。然而,这种区别仅对于像鼠标事件这样的物理事件有效。对于更抽象的事件来说,不需要与可视树中的某个元素建立直接关系(就像由于键盘支持的Click),WPF会传递相同的对象给Source和OriginalSource。
3.3.3 路由事件实践
UIElement类为键盘、鼠标、指示笔输入定义了许多路由事件。大多数路由事件是冒泡事件,但许多事件与管道事件是配对的。管道事件很容易被识别,因为按照惯例,它们的名字中都有一个Preview前缀,在它们的配对冒泡事件发生之前,这些事件会立即被触发。例如,PreviewMouseMove就是一个管道事件,在MouseMove冒泡事件之前被触发。
为许多不同的行为提供一对事件是为了给元素一个有效地取消事件或者在事件即将发生前修改事件的机会。根据惯例,(当定义了冒泡和管道的事件对之后)WPF的内嵌元素只会在响应一个冒泡事件时采取行动,这样可以保证管道事件能够名副其实地做到“预览”。例如,假设需要实现一个TextBox,在该TextBox中对它的输入做了严格的限制,从而保证输入的内容是某种模式或者正则表达式(例如电话号码或者邮政编码)。如果你处理了TextBox的KeyDown事件,你最应该做的事是移除已经显示在TextBox中的文本。但是,如果你处理TextBox的PreviewKeyDown事件,可以标记它为“已处理”,这样不仅可以停止管道事件,还可以防止冒泡的KeyDown事件的触发。在这种情况下,TextBox将不会收到KeyDown通知,而当前输入的字符也不会被显示。
使用指示笔事件
指示笔是一种类似于笔的平板电脑(TabletPC)使用的设备,其默认的行为很像鼠标。换句话说,使用它可以触发一些事件,如MouseMove、MouseDown和MouseUp事件。这一行为对于指示笔至关重要,使它可以在任何程序中使用,而不仅仅局限于平板电脑。然而,如果你打算提供一种针对指示笔优化的体验,可以处理一些指示笔专用的事件,例如StylusMove、StylusDown和StylusUp。指示笔要比鼠标更有“技巧”,因为指示笔的有些事件是没有对应的鼠标事件的,如StylusInAirMove、StylusSystemGesture、StylusInRange和StylusOutOfRange。还有其他一些方式可以挖掘指示笔的功能,而不需要直接处理这些事件。在下一章中,将讲解如何把指示笔与强大的InkCanvas元素结合。
为了演示简单冒泡事件的使用,代码清单3-7更新了原来代码清单3-1中的About对话框,为Window元素的MouseRightButtonDown事件增加了一个事件处理程序。代码清单3-8是实现该事件处理程序的C#代码隐藏文件。
代码清单3-7 About对话框,其中在Window根元素上添加了一个事件处理程序
代码清单3-8 代码清单3-7对应的代码隐藏文件
每当鼠标右击产生一个冒泡事件传递给Window,AboutDialog_MouseRightButtonDown事件处理程序会执行两个动作:首先它会把事件的信息打印到Window的标题栏中,然后会在逻辑树中的被右击的这个元素周围添加(最终会移除)一个较厚的黑边框。图3-7是运行结果。要注意,右击Label将导致Source被设置为那个Label元素,而OriginalSource则是Label的TextBlock可视子元素。
图3-7 右击第一个Label控件之后的About对话框 |
中,然后会在逻辑树中的被右击的这个元素周围添加(最终会移除)一个较厚的黑边框。图3-7是运行结果。要注意,右击Label将导致Source被设置为那个Label元素,而OriginalSource则是Label的TextBlock可视子元素。
如果你运行该示例并右击每一个元素的话,会发现两个有趣的行为:
·右击任何一个ListBoxItem时,Window元素不会收到MouseRightButtonDown事件,这是因为ListBoxItem内部已经处理了这个事件,与MouseLeftButtonDown事件(终止冒泡)一样,以便实现item(项)的选择。
·如果右击一个Button元素,Window会收到MouseRightButtonDown事件,但是设置Button的边界属性不会有任何可视效果。这是Button的默认可视树造成的,可以回到图3-3去看一下,与Window、Label、ListBox、ListBoxItem和StatusBar不同,Button的可视树没有Border元素。
处理单击鼠标中键的事件在哪里?
如果你浏览一遍由UIElement或ContentElement提供的所有鼠标事件,可以找到MouseLeftButtonDown、MouseLeftButtonUp、MouseRightButtonDown和MouseRightButtonUp事件(还有每个事件的管道Preview版本),但是有些鼠标上出现的附加按键该怎么办呢?
这一信息可以通过更加通用的MouseDown和MouseUp事件获得(这两个事件也有对应的Preview事件)。传入这样的事件处理程序的参数包括一个MouseButton枚举值,它表示鼠标状态刚刚改变,它们是Left、Right、Middle、XButton1或XButton2,还有一个对应的MouseButtonState枚举值,表示这个按钮是Pressed还是Released。
终止路由事件是一种假象
虽然在事件处理程序中设置RoutedEventArgs参数的Handled属性为true,可以终止管道传递或冒泡,但是进一步沿着树向上或向下的每个处理程序还是可以收到这些事件!这只能在过程式代码中完成,使用AddHandler的重载添加一个布尔型的参数handledEventsToo。
例如,可以在AboutDialog类的构造函数中把事件特性从代码清单3-7中清除,替换为下面的AddHandler调用。
把true传递给第三个参数意味着:当你用鼠标右键单击一个ListBoxItem时,AboutDialog_ MouseRightButtonDown现在已经收到事件了,并可以添加黑色边框!
在任何时候,都应该尽可能地避免处理已处理过的事件,因为事件应该是在第一时间被处理的。向一个事件的Preview版本添加一个处理程序是不错的替代方法。
总之,终止管道传递或者冒泡仅仅是一种假象而已。更加准确的说法应该是,当一个路由事件标记为已处理时,管道传递和冒泡仍然会继续,但默认情况下,事件处理程序只会处理没有处理过的事件。
3.3.4 附加事件
当树上的每个元素都有路由事件时,该事件的管道传递和冒泡是很自然的。但是WPF路由事件的管道传递和冒泡,甚至可以通过一个没有定义过该事件的元素来完成!这得感谢附加事件。
附加事件与附加属性操作起来很像(它们使用管道传递或冒泡的方式,也与使用具有属性值继承的附加属性的方式非常相似)。代码清单3-9又一次改变了About对话框,这次处理了由ListBox触发的SelectionChanged冒泡事件,以及由Window根元素上的两个按钮触发的Click冒泡事件。由于Window并没有定义它自己的SelectionChanged事件或Click事件,因此事件特性名称必须拥有定义这些事件的类名称作为前缀。代码清单3-10中是实现这两个事件处理程序的代码隐藏文件,这两个事件处理程序只显示了一个MessageBox(消息框),说明刚刚发生了什么。
代码清单3-9 About对话框,其中在Window根元素上添加了两个附加事件处理程序
代码清单3-10 代码清单3-9对应的代码隐藏文件
每个路由事件都可以被当作附加事件使用。代码清单3-9中使用的附加事件语法是有效的,因为XAML编译器发现了位于ListBox上的.NET事件SelectionChanged,以及位于Button上的.NET事件Click。然而,在运行时模式下,会直接调用AddHandler向Window添加这两个事件。因此,这两个事件特性等价于在Window的构造函数中放入了以下代码:
巩固路由事件处理程序
由于要传递许多信息给路由事件,如果真的需要,可以用上层的“megahandler”来处理每一个管道或者冒泡事件。这个处理程序通过分析RoutedEvent对象判断哪个事件被触发了,并把RoutedEventArgs参数转换为一个合适的子类(如KeyEventArgs、MouseButtonEventArgs等),然后继续。
例如,可以把代码清单3-9变为将GenericHandler方法同时赋给ListBox.SelectionChanged和Button.Click事件,定义如下:
由于在.NET Framework 2.0版本中加入了委托逆变特性(delegate contravariance feature),允许把一个委托用于一个方法,该方法的签名使用了一个基类作为参数(例如,使用RoutedventArgs而不是SelectionChangedEventArgs)。当需要获得关于SelectionChanged事件的额外信息时,GenericHandler就会把RoutedEventArgs参数强制转换。
3.4 命令
WPF提供了内建的命令支持,这是一个更为抽象且松耦合的事件版本。尽管事件是与某个用户动作(如点击一个Button或者选中一个ListBoxItem)相关联的,但命令表示的是那些与用户界面分离的动作,最标准的命令示例是剪切(Cut)、复制(Copy)和粘贴(Paste)。应用程序总能通过许多同步的机制提供这些动作:Menu(菜单)控件中的MenuItem(菜单项)、ContextMenu(上下文菜单)控件中的MenuItem、ToolBar(工具栏)控件中的Button(按钮)、键盘快捷方式等。
可以通过事件很好地处理多个命令(如剪切、复制和粘贴)。例如,可以为3个动作分别定义一个通用的事件处理程序,然后把每个处理程序添加到相关元素上的适当事件(如Button的Click事件、主窗口的KeyDown事件等)。另外,当有对应的动作失效时,很可能需要启用或者禁止适当的控件(例如,当剪切板中没有任何东西时,禁止用户界面中的粘贴操作)。这种双向的交流方式会使问题变得复杂,特别是当你不想硬编码一些需要更新的控件的时候。
幸运的是,WPF的命令正好对症下药。这一功能减少了代码数量(在某些情况下,可以不需要任何过程式代码),这样就可以更加灵活地改变你的用户界面了,而不需要破坏后台的逻辑。但命令并不是WPF的新发明,一些较早的技术如微软基础类库(Microsoft Foundation Classes,MFC)也有类似的机制。当然,即使你很熟悉MFC,WPF的命令仍然有它们自己独特的优点需要你去学习。
大多数命令的能力来自于下面3种特性:
WPF定义了许多内建命令。
命令自动支持输入手势(input gesture),如键盘快捷方式。
有些WPF控件有一些与不同命令关联的内建行为。
3.4.1 内建命令
命令是任何一个实现了ICommand接口(位于System.Windows.Input命名空间)的对象,每个对象定义了3个简单的成员:
Execute——执行特定命令的逻辑的方法。
CanExecute——如果命令允许被执行,则该方法返回true;如果不允许执行,则返回false。
CanExecuteChanged——无论何时,只要CanExecute的值改变,该事件就会触发。
如果需要创建剪切、复制和粘贴命令,可以定义3个实现ICommand接口的类,找一个地方存储这3个类(或许可以放在主窗口的静态成员中),从相关的事件处理程序中调用Execute(当CanExecutre返回true时),处理CanExecuteChanged事件,改变相关用户界面中的IsEnabled属性。然而,这听上去并不比只使用事件好多少。
幸好像Button、CheckBox和MenuItem这样的控件有相关的逻辑会与任何命令做交互。它们会有一个简单的Command属性(类型为ICommand),当设置了Command属性后,无论何时Click事件触发,这些控件会自动调用命令的Execute方法(只要CanExecute返回true时)。另外,它们会自动保持IsEnabled的值与CanExecute的值同步,这是通过CanExecuteChanged事件实现的。通过这种给属性赋值的方式,任何逻辑在XAML下都是可以实现的。
更加幸运的是,WPF甚至已经定义了一系列命令,因此不需要为Cut、Copy和Past命令实现ICommand对象,也不用担心在哪里保存这些命令。WPF有5个类的静态属性实现了WPF的内建命令:
ApplicationCommands——Close、Copy、Cut、Delete、Find、Help、New、Open、Paste、Print、PrintPreview、Properties、Redo、Replace、Save、SaveAs、SelectAll、Stop、Undo等。
ComponentCommands——MoveDown、MoveLeft、MoveRight、MoveUp、ScrollByLine、ScrollPageDown、ScrollPageLeft、ScrollPageRight、ScrollPageUp、SelectToEnd、SelectToHome、SelectToPageDown、SelectToPageUp等。
MediaCommands——ChannelDown、ChannelUp、DecreaseVolume、FastForward、IncreaseVolume、MuteVolume、NextTrack、Pause、Play、PreviousTrack、Record、Rewind、Select、Stop等。
NavigationCommands——BrowseBack、BrowseForward、BrowseHome、BrowseStop、Favorites、FirstPage、GoToPage、LastPage、NextPage、PreviousPage、Refresh、Search、Zoom等。
EditingCommands——AlignCenter、AlignJustify、AlignLeft、AlignRight、Correct- SpellingError、DecreaseFontSize、DecreaseIndentation、EnterLineBreak、EnterParag- raphBreak、IgnoreSpellingError、IncreaseFontSize、IncreaseIndentation、MoveDown- ByLine、MoveDownByPage、MoveDownByParagraph、MoveLeftByCharacter、MoveLeftByWord、MoveRightByCharacter、MoveRightByWord等。
每个属性并不会返回实现ICommand的独特类型,相反,它们都是RoutedUICommand的实例。RoutedUICommand类不仅实现了ICommand接口,还可以像路由事件一样支持冒泡。
About对话框有一个Help Button,目前它什么都没做,这样就让我们在Help命令中添加一些逻辑代码,演示一下这些内嵌命令如何工作,其中的Help命令是在ApplicationCommands中定义的。假设这个按钮叫作helpButton,可以用下面的C#代码把Help命令与之关联起来:
所有的RoutedUICommand对象定义了一个Text属性,其中包含了命令的名称,适合显示在用户界面中。(该属性是RoutedUICommand和它的基类RoutedCommand唯一不同的地方。)例如,Help命令的Text属性是(不要感到惊讶)Help字符串。在这个Button上对Content做的硬编码可以用下面的代码替代:
如果你本来想在这次更改后运行About对话框,会发现这个Button现在被永远禁用了。这是因为内嵌命令不可能知道在什么时候应该启用或禁用,也不知道应该执行哪个动作,它们把这些逻辑委托给命令的使用者。
要插入自定义逻辑,需要向元素或父元素添加一个CommandBinding对象,它就会执行该命令(这多亏了有路由命令的冒泡行为)。所有派生自UIElement(和ContentElement)的类包含了一个CommandBindings集合,其中有一个或多个CommandBinding对象。因此,可以向About对话框的根元素Window中添加一个Help的CommandBinding对象,下面是它的代码隐藏文件:
提示 Text字符串是由每个RoutedUICommand定义的,WPF会自动对Text作本地化,并把它转换为任何一种WPF支持的语言。这意味着,如果一个按钮的Content属性被设置为ApplicationCommands.Help.Text,那么如果当前线程的UI文化是西班牙文(Spanish)而不是英文(English),它将自动显示“Ayuda”而不是“Help”。尽管在某些上下文中需要提供的是图像而不是文字(可能是在一个工具栏中),仍然可以在其他地方使用该本地化字符串,例如在ToolTip(工具栏提示)中。当然,你还是需要对你自己的字符串做本地化,这些字符串将在你的用户界面中显示。调整命令的Text属性可以截断你需要翻译的术语的数量。
这里假设HelpExecuted和HelpCanExecute方法已经定义。这些方法将在适当的时候被回调,这样可以插入Help命令的CanExecute和Execute方法的实现。
代码清单3-11和代码清单3-12最后一次改变About对话框,其中完全使用XAML将Help Button与Help命令绑定(虽然两个处理程序必须在代码隐藏文件中定义)。
代码清单3-12 代码清单3-11对应的代码隐藏文件
Window的CommandBinding属性之所以能在XAML中设置,是因为它定义了一个默认的构造函数,允许通过属性设置它的数据。Button的Content也可以在XAML中设置,可以设置它为选中的命令的Text属性,这多亏了有第9章中所讲到的时尚的数据绑定技术。另外,类型转换器简化了XAMl中指定Help命令的过程。CommandConverter类能够识别所有的内建命令,因此在这两处可以设置Command属性为Help,而不用使用更冗长的表达式{x:Static ApplicationCommands.Help}(但无法对自定义命令做这种特殊处理。)在代码隐藏文件中,HelpCanExecute一直把那个命令保持在启用(enabled)状态,而HelpExecuted启动了一个Web浏览器,并加载了一个适当的帮助URL。
3.4.2 使用输入手势执行命令
在这样一个简单的对话框中使用Help命令,可能就像使用一个简单的Click事件处理程序,但是它还有额外的好处(不仅仅是本地化文本):它会自动绑定到一个键盘快捷方式上。
当用户按下F1键时,应用程序会调用帮助。当代码清单3-10中定义的对话框显示在屏幕上时,如果你按下F1键,Help命令会自动被加载,就像按Help Button一样!这是因为像Help这样的命令会定义一个默认的输入手势(input gesture),该手势会执行这个命令。当把KeyBinding和MouseBinding对象添加到相关元素的InputBindings集合中时,就可以把自己的输入手势绑定到一个命令上。例如,如果要设定F2作为执行Help命令的键盘快捷方式,需要在AboutDialog构造函数中添加下面语句:
然而,这样的话F1和F2都会执行Help命令。如果把F1绑定到一个特殊的NotACommand命令上,可以改变F1的默认行为,如下所示:
这两句语句可以用下面的XAML语句替代:
3.4.3 带有内建命令绑定的控件
似乎有些神奇,但是WPF中的一些控件确有自己的命令绑定。最简单的例子就是TextBox控件,它有自己的Cut、Copy和Paste命令的内建绑定,这些命令可以与剪贴板交互,还有Undo和Redo命令的内建绑定。这不仅仅意味着TextBox会响应标准的Ctrl+X、Ctrl+C、Ctrl+V、Ctrl+Z、Ctrl+Y键盘快捷方式,其他元素能很容易地参与到这些动作中来。
下面的XAML展示了这些内建命令绑定的力量:
可以把这些内容放到XamlPad中,或者保存为一个.xaml文件,然后在IE中浏览,因为这里不需要任何程序代码。这5个按钮每一个都与一个命令相关联,它们会把Content属性设置为每个命令的Text属性中的字符串。这里唯一新的就是每个Button的CommandTarget属性设置,该属性指向那个TextBox实例(这里仍然是通过数据绑定功能完成的,这将在第9章中讲解)。这会使命令从TextBox被执行,而不是从Button那被执行,其实这很有必要,因为这样它才能对命令做出响应。
这段XAML生成的结果在图3-8中。如果TextBox中没有文本被选中,前两个Button将被自动禁用;而当有文本被选中时,这两个按钮将被自动启用。类似地,当剪贴板中有文本内容的时候,粘贴Button会自动被启用;如果没有,就会自动被禁用。
图3-8 5个按钮在没有程序代码的情况下仍然可以运行自如,这多亏了TextBox的内建绑定功能 |
虽然通过命令,Button和TextBox可以实现很多的交互,但它们互相并不了解。这也可以说明为什么WPF的内建命令是如此重要。随着第三方用于规范WPF内建命令的控件越来越多,控件之间的交互将越来越趋于无缝,虽然这些控件彼此不了解。
3.5 漫游类层次
WPF类有深度继承层次,因此要把不同的类和它们之间的关系搞清楚很困难。本书封二有一张类地图,当遇到新类时可以查阅它,理清它们的位置和关系。由于空间有限此图并不完整,但其中涵盖了大多数的类。
有一批类是WPF内部工作机制的基础,在继续下面的讲解之前,我们先对这些类做一个快速的解释,其中有一些已经在之前的讲解中带过了。图3-9显示了这些重要的类以及它们的关系,它是类地图的一部分。
这10个类有以下一些显著的特点:
·Object类——所有.NET类的基类。
·DispatcherObject类——只能在创建它的线程上访问的对象的基类。大多数WPF类派生自DispatcherObject,因此都继承了非线程安全特性。这里名字中的Dispatcher是指WPF中类似Win32的消息循环的东西,将在第7章中讲解。
·DependencyObject类——支持依赖属性的任何一个对象的基类。DependencyObject定义了GetValue和SetValue方法,是依赖属性运作的中心方块。
图3-9 WPF的核心类 |
·Freezable类——出于性能原因,可以被“冻结”为一个只读状态对象的基类。一旦被冻结,Freezable对象可以在多个线程中安全共享,这与其他DispatcherObject对象是不同的。冻结的对象就不能被解冻,但是可以复制这些冻结对象来创建一个非冻结的副本。
·Visual类——拥有自己的可视呈现的对象的基类。在第11章中,将深入讨论Visual对象。
·UIElement类——支持路由事件、命令绑定、布局和焦点的可视对象的基类。
·ContentElement类——类似于UIElement的基类,但是它是为那些没有呈现行为的内容准备的。ContentElement寄宿于派生自Visual的类中,然后呈现在屏幕上。
·FrameworkElement类——支持样式、数据绑定、资源和一些通用机制的Windows控件的基类,如tooltips(工具提示)和context menus(上下文菜单)。
·FrameworkContentElement类——类似于FrameworkElement的类,用于表示内容。在第14章中分析了WPF中的FrameworkContentElement对象。
·Control类——一些耳熟能详的控件的基类,如Button、ListBox和StatusBar.Control类,在FrameworkElement基类的基础上添加了很多属性,例如Foreground、Background和FontSize。Controls也支持一些模板,可以完全替代它们的可视树,这些将在第10章中讲解。在下一章中将深入讲解WPF的控件。
在本书中,我们用简单的术语元素指派生自UIElement或FrameworkElement的对象,有时也指派生自ContentElement或FrameworkContentElement的对象。UIElement和FrameworkElement之间或ContentElement和FrameworkContentElement之间的区别并不重要,因为WPF不会带有UIElement和ContentElement的任何公开子类。
3.6 小结
在这一章和前面的两章中,已经学习了在.NET Framework之上构建WPF的主要方式。WPF团队本可以通过一些典型的.NET API提供一些类似于Windows Forms的特性,这也会是一个很有趣的技术。但WPF团队并没有这么做,而是增加了一些基础概念来提供许多特性,这样大大提高了开发人员和设计师的生产效率。
确实,当你聚焦这些核心概念时(本章就是关注这些核心概念的),会看到这并没有曾经的技术那么简单。有许多类型的属性、多种类型的事件、多棵树以及实现同一结果的多种方式(例如用声明式还是过程式代码),希望你现在对这些新机制的价值表示认同。在接下来的内容中,这些概念通常将被淡化,因为我们将集中精力完成特定的开发任务。
由于在本章中使用了一些(原始的)示例,你现在应该对有些WPF控件以及如何布置WPF用户界面有感觉了吧!接下来的三章将基于这些知识展开,正式开始介绍WPF的控件和布局机制。