【WPF学习】第二十九章 元素绑定——将元素绑定到一起
数据banding的最简单情形是,源对象时WPF元素而且源属性是依赖性属性。前面章节解释过,依赖项属性具有内置的更改通知支持。因此,当在源对象中改变依赖项属性的值时,会立即更新目标对象中的绑定属性。这正是我们所需要的行为——而且不必为此构建任何额外的基础结构。
为理解如何将一个元素绑定到另一个元素,下面创建一个简单的示例。该示例窗口包含了两个控件:一个Slider控件和一个具有单行文本的TextBlock控件。如果向右拖动滑动条上的滑块,文本字体的尺寸会立即随之增加。如果向左拖动滑块,字体尺寸会缩小。
显然,使用代码创建这种哦弄个行为不是很难。可简单地响应Slider.ValueChanged事件,并将滑动条控件的当前值复制到TextBlock控件来实现这种行为。不过,通过数据绑定实现这种行为更简单。
一、绑定表达式
当使用数据绑定时,不必对源对象(在本例中是Slider控件)做任何改动。只需要配置源对象使其属性具有正确的值范围,通常进行如下配置:
<Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" IsSnapToTickEnabled="True" TickPlacement="TopLeft"></Slider>
绑定时在TextBlock元素中定义的。在此没有使用字面值设置FontSize属性,而是使用了一个绑定表达式,如下所示:
<TextBlock Margin="10" Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value}" Text="Simple Text"></TextBlock>
数据绑定表达式使用XAML标记扩展(因此具有花括号)。因为正在创建System.Windows.Data.Binding类的一个实例,所以绑定表达式以单词Binding开头。尽管可采用多种方式配置Binding对象,但本例中只需要设置两个属性:ElementName属性(指示源元素)和Path属性(指示源元素中的属性)。
之所以使用名称Path而不是Property,是因为Path可能指向属性的属性(如FontFamily.Source),也可能指向属性使用的索引器(如Content.Children[0])。可构建具有多级层次的路径,使其指向水属性的属性的属性,一次类推。
如果希望引用附加属性(在另一个类中定义但应用于绑定元素的属性),那么需要在圆括号中封装属性名称。例如,如果绑定到Grid控件中的某个元素,路径(Grid.Row)将检索放置元素的行的行号。
二、绑定错误
WPF不会引发因此来通知与数据绑定相关的问题。如果指定的元素或属性不存在,那么不会受到任何指示;相反,指示不能在目标属性中显示数据。
咋一看,对调试而言这像是可怕的梦魇。幸运的是,WPF输出了绑定失败细节的跟踪信息。当调试应用程序时,该信息显示在Visual Studio的Output窗口中。
当试图读取源属性时,WPF会忽略抛出的任何异常,并不加提示地丢弃因数据无法转换为目标属性的数据类型而引发的异常。然而,当处理这些问题时还有一种选择——可通知WPF改变源元素的外观以指示发生了错误。例如,当使用哦感叹号图标或红色轮廓标识非法输入。
三、绑定模式
数据绑定的一个特性是目标会被自动更新,而不考虑源的修改方式。在这个示例中,源只能通过一种方式进行修改——通过用户与滑动条上滑动进行的交互。下面分析该例的一个稍经修改的版本:添加一个按钮,每个按钮为滑动条应用一个预先设置的值:
当单击Set to Large按钮时,会运行下面的代码:
private void cmd_SetLarge(object sender, RoutedEventArgs e) { sliderFontSize.Value = 30; }
上面的代码设置滑动条的值,这会通过数据绑定强制改变字体大小。效果与移动滑动条上的滑块一样。
然而,下面的代码不能正常工作:
private void cmd_SetLarge(object sender, RoutedEventArgs e) { lblSampleText.FontSize = 30; }
上面的代码直接设置文本框的字体尺寸。因此,滑动条的位置未响应地更新。更糟的是,上面的代码破坏了字体尺寸的绑定,并用字面值代替了绑定。如果现在移动滑动条上的滑块,文本框根本不会响应地进行改变。
有趣的是,可采用一种方式强制在两个方向传递数据:从源到目标以及从目标到源。技巧是设置Binding对象的Mode属性。下面的是修订后过的双向绑定,该绑定允许为源或目标应用变化,并使整体的其他部分自动更新自身:
<TextBlock Margin="10" Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value,Mode=TwoWay}" Text="Simple Text"></TextBlock>
在这个示例中,没有理由使用双向绑定(这需要更大的开销),因为可通过使用正确的编码来解决问题。然而,考虑该例的一个变体,该变体包含一个可在其中精确设置字体尺寸的文本框。这个文本框需要使用双向绑定,从而当通过另一个方法改变字体尺寸时,该文本框可以应用用户的改变,并显示最新的尺寸值。
当设置Binding.Mode属性时,WPF允许使用5个System.Windows.Data.BindingMode枚举值中的任何一个。下表列出了全部枚举值:
表 BindingMode枚举值
下图显示了他们之间的区别。前面已经介绍了OneWay和TwoWay模式。OneTime模式非常简单。下面对其他两种模式再进行一些分析。
图 绑定两个属性的不同方式
1、OneWayToSource模式
你可能会好奇,既然有了OneWay模式,为什么还有OneWayToSource模式选项——毕竟这两个值都以相同方式创建单向绑定。唯一区别是绑定表达式的放置位置。本质行,OneWayToSource模式允许通过在通常被视为绑定源的对象中放置绑定表达式,从而翻转源和目标。
使用这一技巧最常见的原因是要设置非依赖项属性的属性。前面开始介绍过,绑定表达式只能用于设置依赖项属性。但通过使用OneWayToSource模式,可克服这一限制,但前提是提供数据的属性本身是依赖项属性。
2、Default模式
最后,除非明确指定其他模式,否则可能认为所有绑定都是单向的,这看起来像是符合逻辑的(毕竟,简单的滑动条示例使用的就是这种方式)。然而,情况并非如此。为了自我验证这一事实,在此考虑具有能够改变字体尺寸的绑定文本框的示例。即使删除了Mode=TwoWay设置,这个示例也仍工作的很好。这是因为WPF使用了一种不同的、默认情况下依赖于所绑定属性的模式(从技术角度看,在每个依赖项属性中都有一个元数据——FrameworkPropertyMetadata.BindsTwoWayByDefault标志——该标志指示属性是使用单向绑定还是双向绑定)。
通常,默认绑定模式也可正是期望的模式。然而,可设想一个示例,该例具有一个只读的不允许用户改变的文本框。对于这种情况,通过将模式设置为单向绑定可稍微降低一些开销。
作为一条常用的经验法则,明确设置绑定模式永远不是坏主意。即使在文本框示例中,也值得通过包含Mode属性来强调希望使用双向绑定。
四、使用代码创建绑定
在构建窗口时,在XAML标记中使用Binding标记扩展来声明绑定表达式通常最高效。然而,也可使用代码创建绑定。
下面的代码演示了上面示例中显示的TextBlock元素创建绑定:
Binding binding=new Binding(); binding.Source=sliderFontSize; binding.Path=new PropertyPath("Value"); binding.Mode=BindingMode.TwoWay; lblSampleText.SetBinding(TextBlock.FontSize,binding);
还可通过代码使用BindingOperation类的两个静态方法移除绑定。ClearBinding()方法使用依赖项属性(该属性具有希望删除的绑定)的引用作为参数,而ClearAllBindings()方法为元素删除所有数据绑定:
BindingOperations.ClearAllBindings(lblSampleText);
ClearBinding()和ClearAllBindings()方法都使用ClearValue()方法,每个元素都从DependencyObject基类继承了ClearValue()方法。ClearValue()方法简单地移除属性的本地值(对于这种情况,是数据绑定表达式)。
基于标记的绑定比通过代码创建的绑定更常见,因为基于脚本的绑定更清晰并且需要完成的工作更少。一般使用标记创建它们的绑定,但在一些特殊情况下,会希望使用代码创建绑定:
- 创建动态绑定:如果希望根据其他运行时信息修改绑定,或者根据环境创建不同的绑定,这时使用代码创建绑定通常更合理(此外,也可在窗口的Resource集合中定义可能希望使用的每个绑定,并只添加是使用合适的绑定对象调用SetBinding()方法的代码)。
- 删除绑定:如果希望删除绑定,从而可以通过普通方式设置属性,需要借助ClearBinding()或ClearAllBindings()方法。仅为属性应用新值是不够的——如果正在使用双向绑定,设置的值会传播到链接的对象,并且两个属性保持同步。
- 创建自定义控件:为让他人能更容易地修改你构建的自定义控件的外观,需要将特定细节(如事件处理程序和数据绑定表达式)从标记移到代码中。
五、使用代码检索绑定
可使用代码检索绑定并检查其属性,而不必考虑绑定最初是用代码还是标记创建的。
可采用两种方式来获取绑定信息。第一种方式是使用静态方法BindingOperations.GetBinding()来检索相应的Binding对象。这需要提供两个参数:绑定元素以及具有绑定表达式的属性。
例如,如果具有如下绑定:
<TextBlock Margin="10" Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value}" Text="Simple Text"></TextBlock>
可使用如下代码来获取绑定:
Binding binding=BindingOperations.GetBinding(lblSampleText,TextBlock.FontSize);
一旦拥有绑定对象,就可以检查其属性。例如,绑定元素名Binding.ElementName提供了绑定表达式的值(这里是sliderFontSize)。Binding.Path提供的PropertyPath对象从绑定对象提取绑定值,Binding.Path.Path获取绑定属性的名称(这里是Value)。还有Binding.Mode属性,用于告知绑定合适更新目标元素。
如果必须在测试时添加诊断代码,绑定对象会有趣一些。但WPF还允许通过调用BindingOperations.GetBindingExpression()方法获得更实用的BindingExpression对象,该方法的参数与GetBinding()方法的参数相同:
BindingExpression expression = BindingOperations.GetBindingExpression(lblSampleText, TextBlock.FontSize);
BindingExpression对象包括一些属性,用于复制Binding对象提供的信息。但迄今为止,最有趣的是ResolvedSource属性,该属性允许计算绑定表达式并获得其结果——传递的本地数据。下面举一个例子:
//Get the source element Slider boundObject=(Slider)expression.ResolvedSource; //Get any data you need from the source element,including it's bound property string boundData=boundObject.FontSize;
六、多绑定
上面的示例仅包含一个绑定,但如有必要,可设置TextBlock元素从文本框中获取其文本,从单独的颜色列表中选择当前前景色和背景色等等,下面是一个示例:
<Window x:Class="DataBinding.MultipleBindings" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MultipleBindings" Height="300" Width="300"> <Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10"></Slider> <TextBox Name="txtContent" Margin="3" Grid.Row="2">Sample Content</TextBox> <ListBox Margin="3" Grid.Row="3" Name="lstColors"> <ListBoxItem Tag="Blue">Blue</ListBoxItem> <ListBoxItem Tag="DarkBlue">Dark Blue</ListBoxItem> <ListBoxItem Tag="LightBlue">Light Blue</ListBoxItem> </ListBox> <TextBlock Margin="3" Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value}" Grid.Row="4" Text="{Binding ElementName=txtContent, Path=Text}" Foreground="{Binding ElementName=lstColors, Path=SelectedItem.Tag}" > </TextBlock> </Grid> </Window>
最终效果如下图所示:
七、绑定更新
下面一个简单的示例,当用户在文本框中输入字体大小时,发现文本字体大小并没有立即变化,而是需要失去当前控件的焦点才会触发。
会发生此问题的愿意,是因为他们的行为由Binding.UpdateSourceTrigger属性控制,该属性可使用下表列出的某个值。当从文本框中取得文本并用于更新TextBlock.FontSize属性时,看到的正式使用UpdateSourceTrigger.LostFocus方法从目标向源进行更新的例子:
表 UpdateSourceTrigger枚举值
请记住,上表列出的值不印象目标的更新方式。他们仅控制TwoWay或OneWayToSource模式的绑定中源的更新方式。
根据上面介绍的内容,可改进文本框示例,从而当用户在文本框中输入内容时将变化应用于字体尺寸。方式如下:
<TextBox Name="txtBound" Text="{Binding ElementName=lblSampleText, Path=FontSize, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Width="100"></TextBox>
要完全控制源对象的更新时机,可选择UpdateSourceTrigger.Explicit模式。如果在文本框示例中使用这种方法,当文本框失去焦点后不会发生任何事情。反而,由编写代码手动触发更新。例如,可添加Apply按钮,调用BindingExpression.UpdateSource()方法,触发立即刷新行为并更新字体尺寸。
当然,在调用BindingExpression.UpdateSource()之前,需要一种方法来获取BindingExpression对象。BindingExpression对象仅是将两项内容封装到一起的较小组装包,这两项内容是:已经学习过的Binding对象(通过BindingExpression.ParentBinding属性提供)和由源绑定的对象(BindingExpression.DataItem)。此外,BindingExpression对象为触发立即更新绑定的一部分提供了两个方法:UpdateSource()和UdateTarget()方法。
为获取BindingExpression对象,需要使用GetBindingExpression()方法,并传入具有绑定的目标属性,每个元素都从FrameworkElement基类继承了该方法。下面的示例根据当前文本框中的文本改变TextBlock的字体大小:
BindingExpression binding=txtFontSize.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
八、绑定延迟
在极少数情况下,需要防止数据绑定触发操作和修改源对象,至少在某一时刻是这样的。例如,可能想在从文本框复制信息之前暂停,而不是在每次按键后获取。或者,源对象在数据绑定属性变化时执行处理器密集型操作。在此情况下,可能更添加短暂的延迟时间,避免过分频繁地触发操作。
在这些特殊情况下,可使用Binding对象Delay属性。等到数毫秒,之后再提交更改。下面是文本框示例的修改版本,会在用户停止输入500毫秒后更新源对象:
<TextBox Name="txtBound" Text="{Binding ElementName=lblSampleText, Path=FontSize, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Delay=500}" Width="100"></TextBox>