数据绑定(data binding)指的是将控件和element连接到数据的一种机制。数据绑定可以很简单,例如将CheckBox控件连接到一个Boolean(布尔)变数;也可以很复杂,将整个数据库连接到一个数据面板(panel)。

在GUI上面呈现控件,一般而言有两种目的,一方面是向使用者显示数据,另一方面是允许使用者改变数据。然而,在现代的API中,许多数据和控件之间的例行连接,都已经被自动化了。在过去,编程员必须写程序代码,用布尔变数的值来初始化一个CheckBox,且将使用者操作过后的CheckBox值写到布尔变量中。在今天的现代化编程环境,程序员只需要定义CheckBox和此变量之间的绑定,就可以了,由数据绑定自动进行这两件工作。数据绑定已经变成一种相当普遍的做法,不只WPF,连Flex/Apollo也采用一样的做法,甚至连语法都很类似。
数据绑定常常用来取代事件处理器(event handler),这么做可以帮助程序代码更简洁,特别是,如果在XAML中使用数据绑定取代事件处理器,可以使得我们不需要在code-behind文件中编写事件处理器。在某些例子中,甚至因此可以完全不用code-behind文件。(当然,事件处理器还是存在,只是被藏了起来,我们目前看不见。)
来源与目标
每个数据绑定都具有来源(source)和目标(target)。一般来说,来源是某种数据,目标则是一个控件,但是实际上,你会发现来源和目标的区别有时候很模糊,有时候似乎会角色错乱,竟然由目标提供数据给来源。虽然来源与目标这种方便的称呼方式,无法明确描述数据的流向,但是来源与目标的区别还是相当重要的。
最简单的绑定,就是两个控件之间的绑定。比方说,假设你想要使用一个Label来观看某ScrollBar的Value property。你可以为ScrollBar安装一个ValueChanged事件处理器,或者你可以用更简单的方式,直接定义一个数据绑定,如下面的XAML片段所展示的这样:
 
<ScrollBar Name="scroll"
    Orientation="Horizontal" Margin="24"
    Maximum="100" LargeChange="10" SmallChange="1" />
 
<Label HorizontalAlignment="Center"
    Content="{Binding ElementName=scroll, Path=Value}" />
 
绑定本身一定是在绑定的目标上做设定。在此XAML片段中,绑定是设定在Label的Content property上,语法如下:
 
Content="{Binding ElementName=scroll, Path=Value}"
 
Binding也是一种markup extension。大括号出现在Binding定义的周围。ElementName与Path都是Binding类别的property,都可以出现在此定义中。在此Binding定义中,ElementName被设定成scroll,这是ScrollBar的名字(指定在Name attribute中);此Binding 的Path property被设定为Value,在这里就是参考到ScrollBar的Value property。此Label的Content property然后会被绑定到此ScrollBar的Value property。当你操作ScrollBar时,Label就会显示出目前的值。
尽管是XAML老手,你还是有可能会不小心在Binding的定义内使用引号。ElementName和Path看起来都像是XML attribute,所以你的手指头会不小心键入这样:
 
Content="{Binding ElementName="scroll" Path="Value"}"
 
这是不对的!不只不可以在大括号内使用引号,而且一定要在ElementName和Path项目之间使用逗号区隔。
另一方面,如果你真的无法控制自己,就是会不小心地在绑定定义内键入引号,那么或许你应该改用property element的语法。
 
<ScrollBar Name="scroll"
    Orientation="Horizontal" Margin="24"
    Maximum="100" LargeChange="10" SmallChange="1" />
 
<Label HorizontalAlignment="Center">
    <Label.Content>
        <Binding ElementName="scroll" Path="Value" />
    </Label.Content>
</Label>
 
绑定的目标不是随便什么都可以,绑定必须建立在有支持dependency property的property上,因为控件和element都是设计成将改变反映在它们的 dependency property。绑定的目标必须衍生自DependencyObject。绑定所设定的property必须有支持dependency property。因此,在这个例子中,Label需要有一个字段元,类型为DependencyProperty,名称为ContentProperty。
但是对于一个绑定来源来说,要求就宽松许多了。来源中被绑定的property,不需要是dependency property。在理想的例子中,property应该和事件有关连,此事件会指出何时此property改变了,但是有些绑定甚至可以在没有通知事件的情况下运作。
模式变化
虽然来源和目标的称呼方式隐含的意义是:来源element(本例是指ScrollBar)会促使目标(Label)的改变,但其实这只是四种可能的绑定模式(mode)之一。你可以利用Mode property和BindingMode列举的成员,指定你想要的模式。预定是这样的:
 
Content="{Binding ElementName=scroll, Path=Value, Mode=OneWay}"
 
请注意,Mode property的设定是和Path property的设定分开的,中间有一个逗号。在BindingMode列举成员OneWay的前后,并没有使用引号。如果你比较喜欢property element语法,你可以改用下面的写法:
 
<Label.Content>
    <Binding ElementName="scroll" Path="Value" Mode="OneWay" />
</Label.Content>
 
你也可以设定模式为TwoWay:
 
Content="{Binding ElementName=scroll, Path=Value, Mode=TwoWay}"
 
此程序的作用其实和OneWay时一样,但是理论上,Label的Content property如果有改变,也会反映在ScrollBar的Value property。下面是另一种可能性:
 
Content="{Binding ElementName=scroll, Path=Value, Mode=OneTime}"
 
OneTime模式的意思是目标会从来源取得数据,进行初始化,但是不会持续追踪改变。在此程序中,Label显示0,因为ScrollBar的初始Value property就是0。如果你操作ScrollBar,你会看到Label的值一直不受影响。(你可以把ScrollBar element的Value初始设定为50,这么一来,你会看到Label显示50。)
最后的选项是:
 
Content="{Binding ElementName=scroll, Path=Value, Mode=OneWayToSource}"
 
初次看到此模式会让人迟疑,因为此模式指示来源要依据目标来更新,这等于是将来源和目标的角色对调。在这个例子中,目标(Label)应该要更新来源(ScrollBar),但是Label没有数字数据可以提供给ScrollBar。此Label是空白的,且会当你移动ScrollBar时,会维持空白。
虽然,OneWayToSource模式似乎反常,但是当你想要建立某种绑定,而目标的property不支持dependency property,且来源的property有支持dependency property时,你就会发现这个模式的妙用了:将绑定放在来源,将模式设定为OneWayToSource。
绑定的预定(default)Mode是由定义此绑定的此property所控制。例如:ScrollBar的Value property预定的绑定Mode是TwoWay。Mode property是绑定最重要的一部份,我们不需要去猜测(或查询)预定的绑定模式为何,而是应该好好考虑,决定该使用什么Mode会比较恰当,然后明确地做设定。
DataContext
DataContext property是另一种表达绑定来源对象的方式,请看下面的例子:
 
<ScrollBar Name="scroll"
    Orientation="Horizontal" Margin="24"
    Maximum="100" LargeChange="10" SmallChange="1" />
 
<Label HorizontalAlignment="Center"
    DataContext="{Binding ElementName=scroll}"
    Content="{Binding Path=Value}" />
 
Label的DataContext与Content,都被设定到一个Binding的定义中,此定义被分成两部分。第一部份的Binding定义指示ElementName,且第二个部分具有Path。
在此范例中使用DataContext property,没有好处,但是在某些其它的例子,DataContext property可能相当有价值。DataContext是可以在element tree中被沿袭,所以如果你为一个element设定DataContext,则该element所有的孩子都会受到影响。
如果一个面板内,许多控件都绑定到一个特定对象的各种property上,这种状况下,只要将此DataContext设定成该型态的不同对象,所有的控件都会反映此新对象。
dependency property的好处
绑定的来源不需要是dependency property。到目前为止,所有的数据绑定范例的目标和来源,都同时具备后端的dependency property,但是本章稍后会有不一样的例子,绑定来源是传统的(没有dependency property)的.NET property。字在一般的例子中,OneWay绑定牵涉到从来源到目标的信息连续传送。单向的绑定想成功,来源必须实现某种机制,以使得来源一有变动就会让目标被通知。
前面所谓的某种机制,当然可能是指事件,但是还有别的可能。dependency property之所以被发明,其中一个原因是为了数据绑定,且dependency property系统具有内建的通知(notification)支持。绑定来源不需要是dependency property,但是如果是的话,会有帮助的。只要定义DependencyProperty,就可以免费得到数据绑定通知。
两个metadata标志影响数据绑定,如果你包含FrameworkPropertyMetadataOptions.NotDataBindable标志,其它的元素flag,仍然可以绑定到dependency property,但是你无法在一个dependency property本身定义一个数据绑定。(换句话说,将具有此flag的dependency property,不会是数据绑定的目标)。FrameworkPropertyMetadataOptions.BindsTwoWayByDefault标志只会影响dependency property为目标的绑定。
前面看过,你将Binding的Path property设定成这些来源对象的property。那么,为何要叫做Path?为何不叫做Property?
之所以称为Path,因为它可以不只一个property。可以是一连串的property(可能附有索引)利用句号结合在一起,看起来像是C#程序代码,但是却没有strong typing的麻烦。例如:
 
Path=Content.Children[1].SelectedItem.Content.Length
 
虽然这可能看起来像是C#程序代码的一连串套迭property,但是这只是XMAL内的字符串,且会被当作字符串进行解析(parse)。解析器使用refelction来决定这些项目是否有意义,如果没有意义,解析的步骤会整个放弃,不产生任何结果或任何例外。
数据转换与多重绑定
当数据从绑定来源送到目的地时(且有时候是反方向的),数据可能需要转换类型。Binding类包含一个property,名为Converter,让你可以指定转换器类。此类必须包含两个方法:Convert与ConvertBack,以进行转换。
进行转换的类必须实现IValueConverter接口,看起来像这样:
 
public class MyConverter: IValueConverter
{
    public object Convert(object value,
       Type typeTarget,object param,
       CultureInfo culture)
    {
        ...
    }
    Public object ConvertBack(object value,
       Type typeTarget,object param,
       CultureInfo culture)
    {
        ...
    }
}
 
这里的value参数是要被转换的对象,而typeTarget是要被转换成的型态,也就是此方法传出值的型态。如果它无法转换成该指定型态,此方法应该要传出null。第三个参数是Binding的ConvertParameter property会指定的对象,最后一个参数用来指示转换时要注意到的地域文化(culture)。
如果你是在C#中建立Binding,你可以将Convert property设定成一个有实现IValueConverter接口的对象:
 
Binding bind = new Binding();
bind.Convert = new MyConverter();
 
你也可以将Binding的ConvertParameter property设定成一个对象,被当作param参数传递进入Convert与ConvertBack,以控制转换过程。
有一种绑定一定会需要转换类别,那就是多重绑定(multi-binding)。多重绑定从多个来源联合多个对象,进入一个单一的目标(例如,将红、绿、蓝三原色合并成为单一个Color对象)。多重绑定转换器,必须实现此IMultiValueConverter接口。
延迟更新
我们目前为止所看过的binding,都从来源source element立即更新目标。有时候这不一定是我们想要的。当你在TextBox键入文字,改变底下数据库的某个字段,你希望每按下一次按键就改变数据库一次吗?其中还包括你打错字并按下退格键做修改的部分。当然不!只有当你完成文字的输入,你才希望来源被更新,而判别这个时机最简单的方式,就是当你此TextBox失去输入焦点的时候。
藉由设定此绑定的UpdateSourceTrigger property,你可以改变更新数据的行为。你可以将它设定为UpdateSourceTrigger列举的一个成员,LostFocus(这是TextBox的Text property预定值)、PropertyChanged(对于大多数的property来说最常见)、或Explicit(这需要程序做出特别的动作,来反映在来源上)。
例如,将绑定改成这样:
 
Text="{Binding ElementName=txtbox1, Path=Text,       
 Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
 
现在每次对TextBox按下按键,来源的TextBox会跟着改变。
UpdateSourceTrigger.Explicit选项需要做更多事才能运作。使用此选项的程序,也必须准备对定义此绑定的element引用其GetBindingExpression方法(这是FrameworkElement所定义的方法),自变量是此绑定所牵涉到的DependencyProperty:
 
BindingExpression bindexp =
    txtboxSource.GetBindingExpression(TextBox.TextProperty);
 
当你想要从目标更新来源时(或许当按下标示Update的按钮时),就引用:
 
bindexp.UpdateSource();
 
此引用无法推翻绑定模式。绑定模式必须是TwoWay或OneWayToSource,否则此引用会被忽略。
Source property
如果你想要开始从数据库以及其它外部类别和对象的角度,思考关于数据绑定的一切,那么需要停用ElementName property,改用Source property。Source property参照到一个对象,而Path继续指到该对象的一个property(或者一连串property)。
对于Source来说,一个可能性是x:Static markup extension。x:Static让XAML文件可以参照到一个类别内的一个静态字段或property。在某些例子(例如用到Environment类别的静态字段),你可以使用x:Static本身取出那些property。然而,有可能你真正需要的是被此静态property所参考到的对象的某个property。这种状况下,你需要透过Source property来使用绑定。
Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐