假设你想让用户与Slider控件进行交互,而且你也想与ColorScroll程序一样,通过TextBlock显示Slider当前的值。其实很简单。只要为Slider控件的ValueChanged事件创建一个处理程序就可以了,当调用处理程序的时候,从Slider取出Value属性的值并将其转换成字符串,然后把该字符串设置给TextBlock的Text属性。
像这样的任务非常普遍,因此Silverlight提供了一种简便的机制来实现这些任务。这种机制称为数据绑定(data binding),或者就简称为绑定(binding)。数据绑定是一个对象的一个属性与另外一个对象的一个属性之间的一条链接(link),因此在绑定的情况下当一个属性发生改变时,另一个属性也随之更新。绑定可以是双向的(bidirectional),在这种情况下,其中一个属性发生的变化会引起另一个属性也随之发生变化。
从本质上说,数据绑定可能像你所期望的那样:由于注册了一个事件处理程序,使得一个属性从另外一个属性中获取更新,期间可能进行了一些数据转换。通常你可以完全通过XAML来定义数据绑定,这意味着你不必编写任何代码。从语法上看,这好像不需要移动任何部件就能传输数据了。
演示数据绑定最简单的方法是使用两个可视化元素,例如Slider和TextBlock元素,我也从这两个元素开始。但是,如果把可视化元素和基础数据源进行绑定的话,更能体现出数据绑定的强大威力。
本章的目标是避免在代码隐藏文件中显式地使用事件处理程序,但是在本章的结束部分我不得不使用几个事件处理程序。当然,我们还是需要一些其他代码来支持XAML中的数据绑定,但这些代码中的大部分可以恰当地归类为业务对象(business object),而不是用户界面元素。
绑定源与目标
在典型的数据绑定中,一个对象的属性发生变化时,另一个对象的属性也随之自动更新。提供数据的对象,例如Slider,被认为是数据绑定的源(source);接收数据的对象(如TextBlock)是绑定的目标(target)。
通常给数据绑定源指定一个名字:
<Slider Name="slider" .../>
你可以把目标属性作为一个属性元素 并赋值给类型为Binding的对象:
<TextBlock ...>
<TextBlock.Text>
<Binding ElementName="slider" Path="Value" />
</TextBlock.Text>
</TextBlock>
使用ElementName属性指定源元素的名称;使用Path属性指定源属性的名称,在这个例子中Path是Slider的Value属性。有时候把这种类型的绑定称为元素名称绑定,因为绑定源是一个可视化元素,并通过名称来引用。
为了使语法变得更加友好,Silverlight为Binding提供了一个标记扩展(markup extension),在此,所有的东西都定义在一对花括号里面。(这是Silverlight for Windows Phone的几个标记扩展中的一个。第7章介绍过StaticResource,第16章将介绍TemplateBinding。)这里是更精简的语法:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}" ... />
请注意,ElementName和Path的设置用一个逗号分隔,而slider和Value名称的引号已经去掉了。引号永远都不会出现在标记扩展的大括号中。
SliderBindings程序使用了这样的绑定方式,你可以试验一下,尝试做一些修改。这一切都在XAML文件里面:
Silverlight项目:SliderBindings 文件:MainPage.xaml(节选)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Slider Name="slider"
Value="90"
Grid.Row="0"
Maximum="180"
Margin="24" />
<TextBlock Name="txtblk"
Text="{Binding ElementName=slider, Path=Value}"
Grid.Row="1"
FontSize="48"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Rectangle Grid.Row="2"
Width="{Binding ElementName=slider, Path=Value}"
RenderTransformOrigin="0.5 0.5"
Fill="Blue">
<Rectangle.RenderTransform>
<RotateTransform x:Name="rotate"
Angle="90" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
这个页面包含了一个范围从0到180的Slider,以及一个TextBlock,TextBlock的Text属性绑定到Slider的Value属性上,另外还有一个Rectangle,它的Width属性也绑定到Slider相同的Value属性上。Rectangle还有一个RotateTransform属性,这个属性使得Rectangle元素旋转了90°。
操作Slider时,TextBlock显示Slider的值,而Rectangle的高度也随之变大或者变小。(Binding的目标是Rectangle的Width属性,而Rectangle的转角是90°。)
在Binding扩展标记中,属性的顺序无关紧要。你可以把Path属性放在前面:
<TextBlock ... Text="{Binding Path=Value, ElementName=slider}"
事实上,如果路径出现第一个位置上,可以删除“Path=”部分,只是使用属性名:
<TextBlock ... Text="{Binding Value, ElementName=slider}"
在本章的后面以及随后的章节中,我将使用这种缩略形式的语法,但对于元素名称绑定,我却不喜欢这样做,因为这样做我就无法知道绑定底层的工作原理。Binding类首先需要在可视化树中找到一个名为slider的元素,然后它需要使用反射来找到该元素的Value属性。我偏向于下面这样的语法,这种语法按照内部操作的过程来排列属性的顺序:
<TextBlock ... Text="{Binding ElementName=slider, Path=Value}"
为什么Binding的这个属性叫做Path而不是Property呢?毕竟,Style类就有一个叫做Property的属性。为什么Binding却没有呢?
简单的答案是Path能把多个属性名组合在一起使用。例如,假设Slider没有名称。如果你知道该Slider是ContentPanel元素的Children集合的第一个子元素,你可以间接地引用Slider:
Text="{Binding ElementName=ContentPanel, Path=Children[0].Value}"
或者,使用可视化树更上一层的元素:
Text="{Binding ElementName=LayoutRoot, Path=Children[1].Children[0].Value}"
Path的组成部件必须是通过点号连接的属性或者索引器(indexer)。
Target和Mode
绑定包含一个源和一个目标。绑定目标被认为是绑定要设置的属性,该属性必须始终都是一个依赖属性,永远都是。当你在代码中创建绑定的时候,这一限制非常明显。
修改一下SliderBindings程序,删除TextBlock的Text属性上的绑定。在MainPage.xaml.cs文件中,你需要添加using指令来引用System.Windows.Data命名空间,这个命名空间包含了Binding类。在构造函数里面,调用完InitializeComponent函数之后,生成一个类型为Binding的对象,并设置它的属性:
Binding binding = new Binding();
binding.ElementName = "slider";
binding.Path = new PropertyPath("Value");
ElementName和Path属性是绑定源。下面看看将TextBlock的Text属性作为绑定目标的代码:
txtblk.SetBinding(TextBlock.TextProperty, binding);
SetBinding方法定义在FrameworkElement里面,第一个参数是依赖属性,也就是目标属性。该目标也是调用SetBinding方法的元素。你也可以使用其他替代方案,使用静态方法BindingOperations.SetBinding来绑定目标:
BindingOperations.SetBinding(txtblk, TextBlock.TextProperty, binding);
但你仍然需要依赖属性。因此,这就是可视化对象的属性应该是依赖属性的另一个原因。你不仅可以为这些属性定制样式,而且可以把它们制作成动画,但所有这些数据绑定的目标必须是依赖属性。
就依赖属性的优先级而论,数据绑定与本地设置的级别相同。
使用BindingOperations.SetBinding方法意味着你可以在任何依赖属性上设置绑定。对于Silverlight for Windows Phone来说,事实并非如此。在Windows Phone中绑定的目标必须是FrameworkElement的属性。
例如,你会发现在MainPage.xaml中的Rectangle元素包含了RotateTransform属性,该属性设置为一个RotateTransform对象。尝试把TextBlock的Text属性和Rectangle的Width属性上的绑定也应用到Angle属性上:
<RotateTransform x:Name="rotate"
Angle="{Binding ElementName=slider, Path=Value}" />
这看起来好像没问题,但却不能正常工作。你会在运行时得到一个XamlParseException异常。Angle本身是依赖属性,不是条件充足了吗?但是RotateTransform并不是派生自FrameworkElement,所以它不可以作为绑定的目标。(在Silverlight 4中,应用于RotateTransform的Angle属性的绑定可以正常工作。但是Silverlight for Windows Phone大体上还是Silverlight 3 。)
如果想要这么做,你需要删除RotateTransform的Angle属性上的绑定,以及已添加到MainPage.xaml.cs的所有代码。把Slider的Value属性的值初始化为90:
<Slider Name="slider"
Value="90" ... />
绑定的目标是TextBlock的Text属性:
<TextBlock Name="txtblk"
Text="{Binding ElementName=slider, Path=Value}" ... />
让我们切换一下,把TextBlock的Text属性初始化为90:
<TextBlock Name="txtblk"
Text="90" .../>
然后把Slider的Value属性作为绑定目标:
<Slider Name="slider"
Value="{Binding ElementName=txtblk, Path=Text}" .../>
乍一看这似乎能正常地工作。Slider的滚动块最初放在中间,这表示Slider的值为90,该值从TextBlock获取,而Rectangle的大小仍然与Slider绑定。然而,当你滑动Slider时,Rectangle的高度改变了,但TextBlock却没有发生任何变化。Slider上的Binding对象正等待着TextBlock的Text属性发生变化,但Text属性没有任何的改变。
现在为Slider上的绑定添加Mode设置,下面的代码表示这个数据绑定是双向(two-way)的。
<Slider Name="slider"
Value="{Binding ElementName=txtblk, Path=Text, Mode=TwoWay}" .../>
现在能正常工作了!绑定的目标仍然是Slider的Value属性。TextBlock的Text属性变化时会影响Slider的Value属性,同样,现在Slider的Value属性变化时也反过来影响到TextBlock。
Mode属性的值为BindingMode枚举类型的成员。Mode属性的默认值是BindingMode. OneWay,除此之外还有BindingMode.TwoWay和BindingMode.OneTime,BindingMode. OneTime表示源只传输一次数据到目标。
使用同样的技巧,可以为RotateTransform的Angle属性建立起绑定关系。首先,把TextBlock上的绑定还原到原始状态:
<TextBlock Name="txtblk"
Text="{Binding ElementName=slider, Path=Value}" .../>
现在为Slider设置双向绑定,指向RotateTransform的Angle属性:
<Slider Name="slider"
Value="{Binding ElementName=rotate, Path=Angle, Mode=TwoWay}" .../>
运行得很好!当滑动Slider时,Rectangle元素也相应地旋转,如图12-1所示。
邓国平