前言
生产工具的先进程度代表了生产力水平。纵观 Windows GUI 应用程序开发工具的发展历史,程序员们在短短十几年就经历了从石器时代到电气时代的变革——从 Windows API、MFC 到 Visual Baisc 再到 .NET Framework。编程工具之所以能代表软件开发的生产力是因为每种软件开发工具背后都隐藏着一整套软件开发的概念和方法。
比如使用 Visual C++ 这个工具进行 Windows API 开发时,我们用不到它所支持的 C++ 功能,仅仅是使用 C 语言的功能、在面相过程的框架内调用 Windows 数以万计的 API 函数、依赖 Windows 的消息机制来创造我们想要的效果;若使用 Visual C++ 进行 MFC 开发,程序员就可以使用 C++ 语言进行面向对象变成了,Windows API 也被封装成与控件对应的类,Windows 的消息被封装成事件的雏形;进入 .NET 时代后,程序设计已经完全组件化,托管的 Visual C++、Visual Basic 和 Visual C# 可以共享组件,同时还建立了完善的 Web 应用程序开发平台……
每套开发的概念和方法实际上就是一套用于解决编程问题、实现客户需求的理论,我们谓之开发的方法论。从 Windows API 到 .NET Framework ,开发的方法论越来越进化,越来越高效。WPF 的开发方法论是在 .NET Framework 方法论的基础上更上一层楼的产物——它完全兼容现有的 Windows Form 开发的方法论,同时又在很多方向进行了升级和创新。下面就是 WPF 开发方法论的一些要素:
- 全新的 UI 设计理念:XAML 语言以及配套工具(包括 Blend 和 Design)。
- 全新的 UI 布局理念:树形结构和各种布局元素。
- 全新的基础类库和控件集:所有控件都在 WPF 方法论的框架下重新设计并放置在 System.Windwos.Control 名称空间里。
- 升级的程序驱动模式:在事件驱动的基础上把事件包装在数据关联(DataBinding)里,变原来的“UI 事件驱动程序运行”为“数据驱动程序运行并显示在 UI 上”,让数据从被动和从属的地位回到了程序的核心地位。
- 升级的属性系统:在 .NET Framework 属性的基础上新增依赖属性(Dependency Property)系统以及其派生出来的附加属性(Attached Property)。
- 升级的事件系统:在 .NET Framework 事件的基础上新增路由事件(Routed Event)系统和基于它的命令系统。
- 升级的资源系统:WPF程序可以使用资源(Resource)存储更丰富的内容并能进行非常方便的检索。
- 全新的模板理念:在 WPF 中,内容决定形式的理念随处可见。如果把控件的功能视为内容,则可以使用控件模板(Control Template)来控制它的展现;如果把数据视为内容,则可使用数据模板(Data Template)把数据展现出来。
- 全新的文档与打印系统:基于 XPS 文档格式,WPF 推出了一整套与文档显示和打印相关的类和控件。
- 全新的 3D 绘图系统:WPF 不但具有 2D 绘图功能,还以完整的类库支持 3D 绘图、视觉和光影效果。
- 全新的动画系统:WPF 具有丰富的动画(Animation)创作类库,以前需要程序员费劲心思才能实现的动画效果现在由设计师使用 XAML 就能实现。
本部分将对 WPF 方法论中的新理念逐一剖析,与大家一起游历 WPF 精彩的内部世界!
友好的 GUI 的流行也就是近十年来的事情,之前的应用程序与用户的交互多是通过控制台界面。我们暂且撇开硬件不谈单说操作系统开发商,就是微软。Windows GUI 运行的机理是使用消息(Message)来驱动程序向前运行,消息的主要来源是用户的操作,比如单击鼠标、按下按钮,都会产生消息,消息又会被 Windows 翻译并送达目标程序然后被程序处理。这听起来并没有什么问题,我们尽管把消息看做是 DOS 命令的升级好了。这种居于操作系统底层的机理势必深刻地影响到应用软件开发的方法论。为了能编写出 Windows 上运行的 GUI 程序,各种开发方法论也必须跟踪这种“消息驱动程序”的基本原理。正是沿着这条路发展,才有了 Windwos API 开发的纯消息驱动、才有了 MFC 等 C++ 类库的消息驱动、才有了 Visual Basic 开始到 .NET Framework 的事件驱使向前的,简称“消息驱动”或“事件驱动”。
消息驱动或事件驱动本身并没有错,但从更高的层次来看,使用“UI驱动程序”开发程序则是“为了 GUI 而 GUI”、单纯地为了实现程序的 GUI 化。实际上这已经背离了程序的本质——数据加算法,同时迫使程序员把很多精力放在了实现 UI 的编程上、这还不算完,随着程序 UI 的日趋复杂,UI 层面上的代码与用于处理数据的逻辑代码也渐渐纠缠在一起变得难以维护。为了避免这样的问题,程序员们总结出了 MVC 和 MVP 等诸多设计模式来吧 UI 相关的代码与数据逻辑相关的代码分离开。
6.1 DataBinding 在 WPF 中的地位
程序的本质是数据加算法。数据会在存储、逻辑和展示三个层流通,所以站在数据的角度上来看,这三层都很重要。但算法在程序中的分布就不均匀了,对于一个三层结构的程序来说,算法一般分布在这几处:
- A. 数据库内部。
- B. 读取和写回数据。
- C. 业务逻辑。
- D. 数据展示。
- E. 界面与逻辑的交互。
A、B 两个部分的算法一般都非常稳定,不会轻易去改动,复用性也很高;C处于客户需求关系最紧密、最复杂,变动也最大,大多数算法都集中在这里;D、E 两层负责 UI 与逻辑的交互,也占有一定量的算法。
显然,C 部分是程序的核心、是开发的重中之重,所以我们应该把精力集中在 C 部分。然而,D、E 两个部分却经常成为麻烦的来源。首先,这两部分都与逻辑层紧密相关,一不小心就有可能把本来该放在逻辑层的算法写进这两部分;其次,这两个部分以消息或事件的方式与逻辑层沟通,一旦出现同一个数据需要在多处展示/修改时,用于同步的代码就会错综复杂;最后,D 和 E 本应该是互逆的一对,但却需要分开来写——显示数据写一个算法、修改数据又是一个算法。总之导致的结果就是 D 和 E 两个部分会占去一部分算法,搞不好还会牵扯不少精力。
问题的根源就在于逻辑层与展示层的地位不固定——当实现客户需求的时候,逻辑层的确处在中心地位,但到了实现UI交互的时候展示层又处于中心地位。WPF 作为一种专门的展示层技术,华丽的外观和动画只是它的表层现象,更重要的是它在深层次上帮助程序员把思维的重心固定在了逻辑层、让展示层永远处于逻辑层的从属地位。WPF 具有这种能力的关键是它引入了 Data Binding 概念以及与之配套的 Dependency Property 系统和 DataTemplate。
展示层则使用 WPF 类库来实现,而展示层与逻辑层的沟通就使用 Data Binding 来实现。
引入 Data Binding 机制后,D、E 两个部分会简化很多。首先,数据在逻辑层与用户界面之间“直来直去”、不涉及逻辑问题,这样用户界面部分几乎不包含算法;Data Binding 本身就是双向通道,所以相当于把 D 和 E合二为一;对于多个 UI 元素关注同一个数据的情况,只需要使用 Data Bingding 把这些 UI 元素一一与数据关联上(以数据为中心的星形结构),当数据变化后这些 UI 元素会同步显示这一变化。更重要的是,经过这样的优化,所有与业务逻辑相关的算法都处在数据逻辑层,逻辑层成为一个能够独立运转的、完整的体系,而用户界面层则不含任何代码、完全依赖和曾属于数据逻辑层。
6.2 Binding 基础
动词Bind在转化为名词Binding后,除了原有的“捆绑”之意外又引申出了“关联”和“键联”的含义。如果把 Binding 比作数据的桥梁,那么它的两端分别是 Binding 的源(Source)和目标(Target)。我们不但可以控制在源和目标之间双向通道还是某个方向的单行道,还可以控制对数据放行的时机,甚至可以设置一些“关卡”用来转换数据类型或者检查数据的正确性。
对Binding有了一个形象的基本概念后,让我们看一个最基本的例子。
首先,我们创建一个名为Student的类,这个类的实例将作为数据源来使用。
public class Student
{
private string _name;
public String Name
{
get { return _name; }
set { _name = value; }
}
}
UI 上的元素关心的是哪个属性值的变化,这个属性就成为 Binding 的路径(Path)。但光有属性还不行——Binding 是一种自动机制,当值变化后属性要有能力通知 Binding,让 Binding 把变化传递给 UI 元素。让一个属性具备这种通知 Binding 值已经变化的能力的方法是在属性的 set 语句中激发一个 PropertyChanged 事件。这个事件不需要我们自己声明,我们要做的是让作为数据源的类实现 System.ComponentModel 名称空间中的 INotifyPropertyChanged 接口。当为 Binding 设置了数据源后,Binding 就会自动来监听这个接口的 PropertyChanged 事件。
实现了 INotifyPropertyChanged 接口的 Student 类看起来是这样:
public class Student: INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _name;
public String Name
{
get { return _name; }
set
{
_name = value;
// 激发事件
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs("Name"));
}
}
}
}
经过这样已升级,当 Name 属性的值发生变化时 PropertyChanged 事件就会被激发,Binding 接收到这个事件后发现事件的消息告诉它是名为 Name 的属性发生了值的变化,于是就会通知 Binding 目标端的 UI 元素显示新的值。
然后,我们在窗体上准备一个TextBox和一个Button。TextBox将作为Binding目标,我们还会在Button的Click事件发生时改变Student对象的Name属性值。
<Window x:Class="DataBinding.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="110" Width="300">
<StackPanel>
<TextBox x:Name="TextBoxName" BorderBrush="Black" Margin="5"/>
<Button Content="Add Age" Margin="5" Click="ButtonBase_OnClick"/>
</StackPanel>
</Window>
接下来,我们将进入最重要的一步——使用 Binding 把数据源和 UI 元素连接起来。C# 代码如下:
public partial class MainWindow : Window
{
private Student stu;
public MainWindow()
{
InitializeComponent();
// 准备数据
stu = new Student();
// 准备 Binding
Binding binding = new Binding();
binding.Source = stu;
binding.Path = new PropertyPath("Name");
// 使用 Binding 连接数据源与 Binding 目标
BindingOperations.SetBinding(this.TextBoxName, TextBox.TextProperty, binding);
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
stu.Name = "Name";
}
}
把数据源和目标连接在一起的任务是使用“BindingOperations.SetBinding(...)”方法完成的。这个方法的 3 个参数是我们记忆的重点:
- 第一个参数用于指定 Binding 的目标,本例中是 this.TextBoxName。
- 与数据源的 Path 原理类似,第二个参数用于为 Binding 指明把数据送达目标的哪个属性。只是你会发现在这里用的不是对象的属性而是类的一个静态只读的 DependencyProperty 类型成员变量!这就是我们后面要详细讲述的与 Binding 息息相关的依赖属性。其实很好理解,这类属性的值可以通过 Binding 依赖在其他对象的属性值上,被其他对象的属性值所驱动。
- 第三个参数很明了,就是指定使用哪个 Binding 实例将数据源与目标关联起来。
实际工作中,实施 Binding 的代码可能与上面看到的不太一样,原因是 TextBox 这类 UI 元素的基类 FrameworkElement 对 BindingOperations.SetBinding(...) 方法进行了封装,封装的结果也是 SetBinding,只是参数列表发生了变化。代码如下:
public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
{
return BindingOperations.SetBinding(this, dp, binding);
}
同时,有经验的程序员还会借助 Binding 类的构造函数及 C# 3.0 的对象初始化器来简化代码。这样一来,上面的代码有可能成为这样:
public MainWindow()
{
InitializeComponent();
// 三合一操作
this.TextBoxName.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = stu = new Student()});
}