《深入浅出WPF》学习总结之Binding

一、前言

  友好的图形用户界面(Graphics User Interface ,GUI)的流行也就是近十来年的事情,之前应用程序与用户的交互是通过控制台界面(Console User Interface ,CUI)完成的。图形用户界面的操作系统开始在中国流行应该是从Windows 95正式发布开始的,旋即冠以Visual的开发工具(以及Borland公司的一些同类产品)也跟着崭露头角。记得那时候硬件能跑起来Windows 95的就已经相当不错了——图形化的界面还是很消耗硬件资源的。

  GUI作为新鲜事物,理所当然的成为了无论是操作系统制造商还是硬件制造商们关注的焦点。我们暂且撇开硬件不谈单说操作系统开发商,也就是微软。Windows GUI 运行的机理是使用消息(Message)来驱使程序向前运行。消息的主要来源是用户的操作。比如单击鼠标、按下按钮,都会产生消息,消息又会被Windows翻译并送达目标程序然后被程序所处理。这听起来并没有什么影响到应用软件开发的方法论。为了编写Windows上运行的GUI程序,各种开发方法论也必须跟从这种“消息驱动程序”的基本原理。正是沿着这条路发展,才有了Windows API 开发的纯事件驱动,才有了MFC等C++类库的消息驱动、才有了Visual Basic开始到.Net Framework的事件驱动——总之一句话,程序是被来自UI的事件(即封装过的消息)驱使向前的。简称“消息驱动”或“事件驱动”。因为消息或事件大都来自于UI,所有统称他们为“UI驱动程序”

  消息驱动或者事件驱动本身并没有错,但从更高的层次上来看,使用“UI驱动程序”开发程序则是“为了GUI而GUI”、单纯的为了实现程序的GUI化,实际上这已经背离了程序的本质——数据+算法。同时迫使程序员把很多精力放在是实现UI的编程上。这还不算完。随着程序的日趋复杂,UI层面上的代码与用于处理数据的逻辑代码也渐渐纠缠在一起变得难以维护。为了避免这样的问题,程序员们总结出了Model-View-Controller(MVC)和Model-View-Presenter(MVP)等诸多设计模式把UI相关的代码与数据逻辑相关的代码分开。

  让我们回归程序的本质。程序的本质是数据+算法,用户给进一个输入,经过算法的处理程序反馈一个输出——这里,数据处于程序的核心地位。反过头来看“UI驱动程序”,数据处于被动地位,总是在等待程序接受来自UI的消息/事件后被处理或者算法完成处理后被显示。如何在GUI编程时把数据的地位由被动变主动、让数据处于程序的核心呢?这就用到了Data Binding

二、Data Binding在WPF中的地位

  如果把一个应用程序看作是一个城市,那么这个城市内部的交通肯定非常繁忙,但川流不息的不是行人和车辆而是数据。一般情况下,应用程序会具有三层结构,即数据存储层、数据处理层和数据展示层。存储层相当于一个城市的仓储区,有数据库和文件系统构成;处理层(逻辑层)与业务逻辑相关、用于加工处理数据的算法都集中在这里,这一层相当于城市的工业区;展示层的功能是把加工后的数据通过可视化的界面展示给用户或者通过其他种类的接口展示给别的应用程序(界面和接口两个词在英文中均为interface,所以本质上没有什么区别),还需要收集用户的操作,把他们反馈给逻辑层,所以这一层相当于城市的港口区。

  如果我们是一名市长,我们就需要对这个城市的布局和发展负责——仓储区、产业园和港口区、我们该怎么投资呢?每个园区下多大力气开发?每个园区内部应该怎么发展?几个区之间的交通如何规划才能整洁高效?怎样为未来的扩建留有余地...这些都是我们该考虑的问题,其实架构师要做的就是这些!

  程序的本质是数据+算法。数据会在存储、逻辑、和展示三个层流通,所以站在数据的角度上来看,这三层都很重要。但算法在程序中的分布就不均匀了,对于一个三层架构的程序来说,算法一般分布在这几处:

  • A、数据库内部。
  • B、读取和写回数据库。
  • C、业务逻辑。
  • D、数据展示。
  • E、界面与逻辑的交互。

  A、B两个部分的算法一般都很稳定,不会轻易去改动,复用性也很高;C处于客户关系最紧密,最复杂,变动也最大、大多数算法都集中在这里;D、E两层负责UI与逻辑的交互,也占有一定的算法。

  显然C部分是程序的核心、是开发的重中之重,所以我们应该把精力集中在C部分。然而D、E两部分却经常为麻烦的来源。首先、这两部分与逻辑层紧密相关,一不小心就可能把本该放在逻辑层里的算法写进这两部分(所以才有了MVC、MVP等模式来避免这种情况出现);其次,这两个部分以消息或事件的方式与逻辑层沟通,一旦出现同一数据需要在多处展示/修改时,用于同步的代码就会错综复杂;最后D和E本该是互逆的一对,但却需要分开来写——显示数据写一个算法,修改数据又是一个算法。总之导致的结果就是D和E两个部分会占去一部分算法,可能还会牵扯不少精力。

  问题的根源就在于逻辑层与展示层的地位不固定——当实现客户需求的时候,逻辑层的确处在中心地位,但到了UI实现交互的时候展示层又处于中心地位。WPF作为一种专门的展示层技术,华丽的外观和动画只是他的表层现象,更重要是它在深层次上帮助程序员把思维重心固定在了逻辑层、让展示层永远处于逻辑层的从属地位,WPF具有这种能力的关键是引入了Data Binding概念以及与之配套的Dependency Property系统和DataTemplate。

  从传统的Windows Form迁移到WPF之后,对于一个三层程序而言,数据存储层由数据库和文件系统来构建。数据传输和处理仍然使用.Net Framework 的ADO.NET等基本类(与Windows Form等开发一样),展示层则使用WPF类库来实现,而展示层与逻辑层的沟通就是用Data Binding来实现。可见,DataBinding在WPF系统中起到的是数据高速公路的作用。有了这条高速公路,加工好的数据会自动送达用户界面加以显示,被用户修改过的数据会自动传回逻辑层,一旦数据被加工好又会被送达用户界面...程序的逻辑层就像一个强有力的引擎不停的运转,用加工好的数据驱动程序的用户界面以文字、图形、动画等形式展示出来——这就是“数据驱动UI”。

  引入Data Binding机制后,D、E两个部分会简化很多。首先,数据在逻辑层与用户界面之间“直来直去”不涉及逻辑问题,这样用户界面部分几乎不包含算法;Data Binding本身就是双向通信,所以相当于把D和E合二为一;对于多个UI元素关注同一个数据的情况,只需要使用Data Binding把这些UI元素一一与数据关联上(以数据为中心的星形结构),当数据变化后这些UI元素会同步显示这一变化。所以前面我们提到的问题就迎刃而解了。更重要的是,经过这样的优化,所有与业务相关的算法都处在数据逻辑层,逻辑层就成为一个独立运转的、完整的体系,而用户界面层则不包含任何代码、完全依赖和从属于数据逻辑层。这样做有两个显而易见的好处,第一,如果把UI层看作是应用程序“皮”、把存储层和逻辑看作是程序的“瓤”,那么我们可以很轻易的把皮从瓤上撕下来并换一个新的;第二,因为数据层能独立运转、自成体系,所以我们可以进行更完善的单元测试而无需借助UI自动化测试工具——你完全可以把单元测试代码想象成一个“看不见的UI”,单元测试只是使用这个“UI”绕过真实的UI直接测试业务逻辑罢了。

三、Binding 基础

  如果把Binding比作数据的桥梁,那么他的两端分别是Binding的源(Source)和目标(Target)数据从哪里来,哪里就是源,Binding是架在中间的桥梁,Binding目标是数据要往哪儿去(所以我们就要把桥架向哪里)。一般情况下,Binding源是逻辑层的对象,Binding目标是UI层的控件对象,这样,数据会源源不断通过Binding送达UI层,被UI层展现,也就完成了数据驱动UI的过程。我们可以想象Binding这座桥梁上铺设了高速公路,我们不但可以控制公路是在原目标之间双向通行还是某个方向的单行道,还可以控制对数据放行的时机,甚至可以在桥上架设一些“关卡”用来转换数据类型或者校验数据正确性。

   对Binding有了形象的基本概念后,让我们看一个最基本的例子。这个例子就是创建一个简单的数据源并通过Binding把它连接到UI元素上。

  首先我们创建一个名为Student的类,这个类的实例将作为数据源来使用。

 1     public class Student
 2     {
 3         private string _name;
 4 
 5         public string Name
 6         {
 7             get { return _name; }
 8             set { _name = value; }
 9         }
10 
11     }

  可以看到Student这个类非常简单,简单到只有一个string类型的Name属性,前面说过,数据源是一个对象,一个对象身上可能有很多数据,这些数据又通过属性暴露给外界,那么,其中那个数据是你想通过Binding送达UI元素的呢?换句话说,UI上的元素关心的是哪个属性值的变化呢?这个属性就成为Binding的路径(Path)。但光有属性还不行——Binding是一种自动机制,当属性变化后属性要有能力通知Binding,让Binding把变化的传递给UI元素。怎样才能让一个属性具备这种通知Binding值已经变化的能力呢?方法是在属性的set语句中计划一个ProertyChanged事件。这个事件不需要我们自己申明,我们要做的是让作为数据源的类实现System.ComponentModel名称空间中的INotifyPropertyChanged接口。当为Binding设置了数据源后,Binding就会自动侦听来自这个接口的PropertyChanged事件。

  实现了INotifyPropertyChanged接口的Student类看起来是这样的:

 1 public class Student : INotifyPropertyChanged
 2 {
 3     private string _name;
 4 
 5     public string Name
 6     {
 7         get { return _name; }
 8         set
 9         {
10             _name = value;
11             if (PropertyChanged != null)
12                 PropertyChanged(this, new PropertyChangedEventArgs("Name"));
13         }
14     }
15 
16     public event PropertyChangedEventHandler PropertyChanged;
17 }

  经过这样一升级,当Name属性的值发生变化时PropertyChanged事件就会被激发。Binding接受到这个事件后发现事件的消息告诉它名为Name的属性发生了值的改变,于是就会通知Binding目标端的UI元素显示新的值。

  然后,我们在窗体上准备一个TextBox和一个Button。TextBox将作为Binding目标,我们还会在Button的Click事件发生时改变Student对象的Name属性值。

 1 <Window x:Class="WpfApp2.MainWindow"
 2         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 3         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 4         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
 5         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
 6         xmlns:local="clr-namespace:WpfApp2"
 7         mc:Ignorable="d"
 8         Title="MainWindow" Height="110" Width="300">
 9     <StackPanel>
10         <TextBox x:Name="txtBoxName" Margin="5" BorderBrush="Black"/>
11         <Button Content="Test" Margin="5" Click="Button_Click"/>
12     </StackPanel>
13 </Window>

   接下来,我们将进行最重要的一步——使用Binding把数据源和UI元素连接起来,代码如下

 1 public partial class MainWindow : Window
 2 {
 3     Student mStdTest;
 4     public MainWindow()
 5     {
 6         InitializeComponent();
 7 
 8         mStdTest = new Student();
 9 
10         var binding = new Binding("Name") { Source=mStdTest};
11         BindingOperations.SetBinding(txtBoxName, TextBox.TextProperty, binding);
12     }
13 
14     private void Button_Click(object sender, RoutedEventArgs e)
15     {
16         mStdTest.Name += "Name";
17     }
18 }

   让我们逐一解读一下这段代码:这段代码是Windows1类的后台部分,它的UI部分是上面给出的XAML代码。“Student mStdTest”是为Windows1类声明了一个Student类型的成员变量,这样做的目的是为了在Windows1的构造器和Button_Click事件处理器中都能访问由它引用的Student实例(数据源)。

  在Windows1的构造器中"InitializeComponent();"是自动生成的代码,用途是初始化UI元素。"mStdTest = new Student();"这句是创建了一个Student类型的实例并用mStdTest成员变量引用它,这个实例就是我们的数据源。

  在准备Binding的部分,先是用”Binding binding = new Binding("Name"){Source = mStdTest};“声明Binding类型变量并创建实例,并在构造函数和构造器里面设置了Path(访问路径)和Source(访问源)。

  把数据源和目标连接在一起的任务是使用"BindingOperations.SetBinding(...);"方法完成的。这个方法的三个参数是我们记忆的重点。

  1. 第一个参数用于指定Binding的目标,本例中是txtBoxName。
  2. 与数据源的Path原理类似,第二个参数用于为Binding指明把数据送达目标的哪个属性。只是你会发现在这里用的不是对象的属性而是类的一个静态只读(Static Readonly)的依赖属性。其实很好理解,这类属性的值可以通过Binding依赖在其他对象的属性值上,被其他对象属性值所驱动。
  3. 第三个参数很明了,就是指定使用哪个Binding实例将数据源与目标关联起来。

  处于末尾的Button_Click...方法是Button元素的单击事件,在他内部我们对数据源的Name属性进行了更新。运行程序,当你单击Button时,TextBox就会即时显示更新后的Name属性值如下:

  实际工作中,实施Binding的代码可能与上面看到的不太一样,原因是TextBox这类UI元素的基类FrameworkElement对BindingOperations.SetBinding(...)方法进行了封装,封装的结果也叫SetBinding,只是参数发生了变化,代码如下:

1 public BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding)
2 {
3     return BindingOperations.SetBinding(this, dp, binding);
4 }

  通过上面的例子,我们已经在脑海中建立了如下模型

四、Binding的源与路径

  Binding的源也就是数据的源头。Binding对源的要求并不苛刻——只要他是一个对象,并且通过属性(Property)公开自己的数据,它就能作为Binding的源。

  上面的例子就已经展示了这个概念。如果想让作为Binding源的对象具有自动通知Binding自己的属性值已经发生了变化的能力,那么就需要让类实现INotifyPropertyChanged接口并在属性的set语句中激发PropertyChanged事件。在日常的工作中,除了使用这种对象作为数据源外,我们还有更多的选择,比如控件把自己或自己的容器或子级元素当源、用一个控件作为另一个控件的数据源、把集合作为ItemsControl的数据源、使用XML作为TreeView或Menu的数据源、把多个控件关联到一个“数据制高点”上,甚至干脆不给Binding指定数据源、让它自己去找。

五、把控件作为Binding源与Binding标记扩展

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="110" Width="300">
    <StackPanel>
        <TextBox x:Name="txtBoxName" Margin="5" BorderBrush="Black" Text="{Binding Path=Value,ElementName=slider}"/>
        <Slider x:Name="slider"  Margin="5"/>
    </StackPanel>
</Window>

 

 除了可以在C#代码中建立Binding外在XAML代码里也可以方便的设置Binding,这就给设计师很大的自由度来决定UI元素之间的关联情况。值得注意的是,在C#代码中可以访问XAML代码中声明的变量但XAML代码中却不可以访问C#代码中声明的变量,因此,想要在XAML中建立UI元素与逻辑对象的Binding还要颇费些周折,把逻辑层对象声明为XAML代码中的资源(Resource)我们把它放在资源一章去解释。

  以上XAML代码,它使用了Binding标记扩展语法:

<TextBox x:Name="txtBoxName" Margin="5" BorderBrush="Black" Text="{Binding Path=Value,ElementName=slider}"/>

与之等价的C#代码为:

this.txtBoxName.SetBinding(TextBox.TextProperty, new Binding("Value") { ElementName = "slider" });

 六、控制Binding的方向及数据更新

  Binding在源与目标之间架起了沟通的桥梁,默认情况下,数据既能通过Binding送达目标,也能从目标返回源(收集用户对数据的修改)。有时候数据只需要展示给用户、不允许用户修改,这时候可以把Binding模式更改为从源向目标的单向沟通,Binding还支持从目标向源的单向沟通。以及只在Binding关系确立时读取一次数据,这需要我们根据实际情况去选择。

  控制Binding数据流向的数据是Mode,它的类型是BindingMode枚举。BindingMode可取值TwoWay、OneWay、OnTime、OneWayToSource和Default。这里的Default是指Binding的模式会根据目标的实际情况来确定,比如若是可编辑的(如TextBox.Text属性),Default就采用双向模式;若是只读的(TextBlock.Text)则采用单向模式。

  接上面的例子,当我们拖动Slider手柄时,TextBox就会显示当前Slider的值。如果我们在TextBox里输入一个恰当的值,然后按下Tab键、让焦点离开TextBox,则Slider的手柄会跳到相应的值那里。

  为什么一定要在TextBox失去焦点之后Slider的值才会改变呢?这就引出了Binding的另一个属性——UpdateSourceTrigger,它的类型是UpDateSourceTrigger枚举,可取值为PropertyChanged、LostFocus、Explicit和Default。显然,对于TextBox默认值的Default的行为与LostFocus一致,我们只需要把这个属性改为PropertyChanged,则Slider的手柄就会随着我们在TextBox里面输入的值而改变位置。

 

posted @ 2020-01-24 18:30  Simple_Han  阅读(556)  评论(0编辑  收藏  举报