binding

来源:百度文库 编辑:神马文学网 时间:2024/04/25 15:21:01
数据绑定是指从一个对象中提取信息,并在应用程序的用户界面中显示所提取的信息,而不用编写枯燥的代码就可以完成所有的工作。通常,富客户端使用双向的数据绑定,这种数据绑定提供了从用户界面向一些对象推出信息的能力—— 同样,不需要或者几乎不需要编写代码。因为许多Windows应用程序都会用到数据(并且所有这些应用程序在某些时候需要处理数据),所以在用户界面技术中数据绑定和WPF一样,也是一个非常重要的概念。
曾经进行过Windows窗体开发的WPF开发人员,会发现WPF数据绑定和Windows窗体数据绑定有许多类似的地方。与在Windows窗体中一样,WPF数据绑定允许创建从任何对象的任何属性获取信息的绑定,并且可以使用创建的绑定填充任何元素的任何属性。WPF还提供了一系列能够处理整个信息集合的列表控件,并且允许通过这些控件定位信息。然而,数据绑定在背后的实现方式却有重大的改变,增加了一些非常强大的新功能,进行了一些修改和细微的调整。虽然使用了许多相同的概念,但是没有使用相同的代码。
在本章,将学习如何使用WPF数据绑定。将会创建声明式的从元素和其他对象提取所需信息的绑定。还将学习如何将这一系统插入到后端的数据库中,不管是计划使用标准的ADO.NET数据对象,还是构建自己的自定义数据类。
16.1  数据绑定基础
简单地说,数据绑定是一个联系,该联系告诉WPF从一个源对象中提取信息,并使用提取到的信息设置目标对象的一个属性。目标属性总是依赖项属性,并且它通常位于WPF元素中—— 毕竟,WPF数据绑定的最终目标是在用户界面中显示一些信息。然而,源对象可以是任何内容,从其他WPF元素到ADO.NET数据对象(如DataTable对象和DataRow对象)或者自己创建的数据对象。在本章,首先通过分析最简单的方法(从元素到元素的绑定)来研究数据绑定,然后分析如何使用具有其他类型对象的数据绑定。
16.1.1  绑定到元素的属性
数据绑定最简单的情况是,源对象是WPF元素并且源属性是依赖项属性。这是因为依赖项属性具有内置的更改通知支持,在第6章中介绍过该内容。因此,当在源对象中改变依赖项属性的值时,会立即更新目标对象中的绑定属性。这正是我们所需要的行为—— 而且为了得到该行为不需要构建任何额外的结构。
注意:
尽管从元素到元素的绑定是最简单的方法,但在现实世界中,大多数开发人员对查找最通用的方法更感兴趣。总之,各种数据绑定都是将元素绑定到数据对象,从而允许显示从一个外部源(如数据库或文件)提取的信息。不过,从元素到元素的绑定通常是很有用的。例如,可以使用从元素到元素的绑定使元素的交互方式自动化,这样当用户修改控件时,另外一个元素会被自动更新。这是很有价值的快捷方式,可以不必编写样板代码(在上一代的Windows窗体应用程序中,这种技术是不可能实现的)。
为了理解如何能够将一个元素绑定到另外一个元素,分析图16-1中显示的简单窗口。在该窗口中包含了两个控件:一个Slider控件和一个具有单行文本的TextBlock控件。如果向右拖动滑竿控件上的滑块,文本的字体尺寸会立即增加。如果向左拖动滑块,字体的尺寸会缩小。
图16-1  通过数据绑定链接的控件
显然,使用代码创建这种行为不是很困难。可以简单地响应Slider.ValueChanged事件,并将滑竿控件的当前值复制到TextBlock控件来实现这种行为。然而,通过数据绑定实现这种行为更简单。
提示:
数据绑定还有另外一个优点—— 它允许创建简单的能够运行于浏览器中的XAML页面,而不用将它们编译进应用程序中(在第1章中学习过,如果XAML页面具有链接的后台代码,就不能在浏览器中打开它)。
当使用数据绑定时,不需要对源对象(在本示例中是Slider控件)进行任何改变。只需要配置源对象使其属性具有正确的值范围,通常进行如下配置:
Minimum="1" Maximum="40" Value="10"
TickFrequency="1" TickPlacement="TopLeft">

绑定是在TextBlock元素中进行定义的。在此没有使用字面值设置FontSize属性,而是使用了绑定表达式,如下所示:
FontSize="{Binding ElementName=sliderFontSize, Path=Value}" >

数据绑定表达式使用了XAML标记扩展(因此具有花括号)。因为正在创建System.Windows. Data.Binding类的一个实例,所以绑定表达式以单词Binding开始。尽管可以使用多种方式配置Binding对象,但是在本示例中只需要设置两个属性:ElementName属性(表示源元素)和Path属性(表示源元素中的属性)。
提示:
使用名称Path而不是Property,是因为Path可能指向一个属性的属性(如FontFamily.Source),也可能指向属性使用的索引器(如Content.Children[0])。可以构建一个具有多级层次的路径指向一个属性的属性的属性,等。
如果希望引用一个附加属性(在另外一个类中定义但是应用到绑定元素的属性),需要在圆括号中包装属性名称。例如,如果绑定到Grid控件中的一个元素,路径(Grid.Row)检索放置元素的行数。
数据绑定的一个特性是目标会被自动更新,而不管源是如何被修改的。在这个示例中,源只能通过一种方式进行修改—— 通过用户与滑竿上的滑块进行交互。下面考虑该示例的一个修改版本,添加几个按钮,每个按钮为滑竿应用一个预先设置的数值。图16-2显示了新的窗口。
图16-2  通过代码修改数据绑定源
当单击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;
}
数据绑定错误
WPF不会引发异常,通知与数据绑定相关的问题。如果指定的一个元素或者属性不存在,不会收到任何指示—— 只是在目标属性中不能显示数据。
乍一看,对于调试这好像是一个可怕的梦魇。幸运的是,WPF输出了绑定失败细节的跟踪信息。当调试应用程序时,该信息显示在Visual Studio的Output窗口中。例如,如果试图绑定到一个不存在的属性,在Output窗口中就会看到和类似下面的信息:
System.Windows.Data Error: 35 : BindingExpression path error:
'Tex' property not found on 'object' ''TextBox' (Name='txtFontSize')'.
BindingExpression:Path=Tex; DataItem='TextBox' (Name='txtFontSize');
target element is 'TextBox' (Name='');
target property is 'Text' (type 'String')
当试图读取源属性时,WPF会忽略抛出的任何异常,并且会默默地丢弃源数据和目标属性的数据类型不匹配时所引发的异常。然而,当处理这些问题时还有另外一种选择—— 可以通知WPF改变源元素的外观,以指示发生了错误。例如,可以使用感叹号图标或者红色轮廓标识非法输入。在本章后面的16.5节中演示了这种技术。
上面的代码直接设置文本框的字体尺寸。因此,滑竿的位置没有进行相应的更新。更糟糕的是,上面的代码摧毁了字体尺寸绑定,并且使用一个字面值代替了绑定。如果现在再移动滑竿上的滑块,文本块根本不会相应地进行改变。
有趣的是,有一种方法可以强制在两个方向传递数值:从源到目标以及从目标到源。技巧是设置Binding对象的Mode属性。下面是一个修订过的双向绑定,该绑定允许应用源或目标的改变,并且能够自动更新自身。
FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}" >

在这个示例中,没有理由使用双向绑定(这需要更大的开销),因为可以通过使用正确的编码来解决问题。然而,该示例的一个变体包含了一个可以准确设置字体尺寸的文本框。这个文本框需要使用双向绑定,从而当通过另外一种方法改变字体尺寸时,该文本框可以应用用户的改变,并显示最新的尺寸值。在后面将会看到这个变体。
16.1.2  使用代码创建绑定
当正在绑定一个窗口时,在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( )方法简单地删除属性的本地值(对于这种情况,是数据绑定表达式)。
基于标记的绑定比通过代码创建的绑定更常见,因为基于标记的绑定更清晰并且只需要做更少的工作。在本章,所有的示例使用标记创建它们的绑定。但在一些特殊的情况下,将会希望使用代码创建绑定:
●       创建动态的绑定。如果希望根据运行时的信息修改绑定,或者根据环境创建不同的绑定,这时使用代码创建绑定通常更合理(此外,可以在窗口的Resources集合中定义可能希望使用的每个绑定,并添加代码使用合适的绑定对象调用SetBinding( )方法)。
●       删除绑定。如果希望删除绑定,从而可以通过普通的方式设置属性,需要使用ClearBinding( )方法或ClearAllBindings( )方法。为属性应用一个新值并不是很简单—— 如果正在使用双向绑定,设置的值会传播到链接的对象,并且两个属性保持同步。
注意:
可以使用ClearBinding( )方法和ClearAllBindings( )方法删除任何绑定,不管是通过代码还是使用XAML标记应用的绑定。
●       创建自定义控件。为了让他人能够更容易地修改您构建的自定义控件的外观,需要将特定的细节(如事件处理程序和数据绑定表达式)从标记中移到代码中。第24章提供了一个自定义的颜色拾取控件,该控件使用代码创建它的绑定。
16.1.3  多绑定
上面的示例只提供了一个单独的绑定,如果需要,可以设置TextBlock元素从一个文本框中获取它的文本,从一个独立的列表框中选择当前的前景色和背景色,等等。下面是一个示例:
FontSize="{Binding ElementName=sliderFontSize, Path=Value}"
Text="{Binding ElementName=txtContent, Path=Text}"
Foreground="{Binding ElementName=lstColors, Path=SelectedItem.Tag}" >

图16-3显示了具有三个绑定的TextBlock元素。
还可以链接数据绑定。例如,可以为TextBox.Text属性创建绑定表达式链接到TextBlock.FontSize属性,TextBlock.FontSize属性包含一个链接到Slider.Value属性的绑定表达式。对于这种情况,当用户将滑竿上的滑块拖动到一个新位置上时,滑竿的值从Slider控件传递到TextBlock控件,然后又从TextBlock元素传递到TextBox元素。尽管这种方法可以无缝地工作,但是一个更加清晰的方法是应当尽可能地将元素直接绑定到它们使用的数据。在此描述的这个示例中,应当考虑将TextBlock控件和TextBox控件都直接绑定到Slider.Value属性。
如果希望目标属性被多个源影响,问题会变得更加有趣—— 例如,如果希望使用两个相同的合法绑定来设置属性,乍一看,这好像不可能。毕竟,当创建一个绑定时,只能指定一个目标属性。然而,可以使用多种方法突破这一限制。
最简单的方法是改变数据绑定的模式。如前所述,Mode属性允许改变绑定的方向,从而使数值不仅仅能够从源传递到目标,而且还可以从目标传递到源。使用这一技术,可以创建设置同一属性的多个绑定表达式。其中,最后设置的属性有效。
为了理解这种方法的工作原理,考虑滑竿示例的一个变体,添加一个能够设置所期望的准确字体尺寸的文本框。在这个示例中(如图16-4所示),可以使用两种方法设置TextBlock.FontSize属性—— 通过拖动滑竿上的滑块或者在文本框中输入字体尺寸。所有的控件都会保持同步,因此如果在文本框中输入一个新的数值,示例文本的字体尺寸就会相应地进行调整,并且滑竿上的滑块也会被移动到相应的位置。
图16-3  绑定到三个元素的TextBlock元素           图16-4  将两个属性链接到字体尺寸
正如您所知道的,可以为TextBlock.FontSize属性应用一个数据绑定。将TextBlock.FontSize属性直接绑定到滑竿是合理的:
FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}" >

尽管不能再为FontSize属性添加另外一个绑定,但是可以将新的控件——  如TextBox控件—— 绑定到TextBlock.FontSize属性。下面是所需要的标记:


现在,无论何时TextBlock.FontSize属性发生变化,当前值都会被插入到文本框中。可以在文本框中编辑数值,应用一个特定的尺寸。注意,为了使该示例能够工作,TextBox.Text属性必须使用双向绑定,从而使数值能够在两个方向上传递。否则,文本框只能够显示TextBlock.FontSize属性的值,但不能够改变TextBlock.FontSize属性的值。
这个示例存在以下几个问题:
●       因为Slider.Value属性是双精度类型,所以当拖动滑竿上的滑块时,得到的字体尺寸数值是小数。可以通过将TickFrequency属性设置为1,并将IsSnapToTickEnabled属性设置为ture,将滑竿的值限制为整数。
●       在文本框中可以输入字母以及其他非数字字符。如果输入了其他字符,文本框的值就不再被解释为一个数值。因此,数据绑定会失败,并且字体尺寸会被设置为0。一个解决方法是处理在文本框中按下的键来阻止非法输入,或者使用数据绑定验证,在本章的后续内容中会讨论数据绑定验证。
●       直到文本框失去焦点之后(例如,当使用tab键将焦点移动到另外一个控件),才会应用文本框中的改变。如果这不是所希望的行为,可以通过Binding对象的UpdateSourceTrigger属性立即进行更新,在稍后的16.1.5节将会介绍相关内容。
有趣的是,在此给出的解决方案不是连接文本框的唯一方法。也可以合理地配置文本框,使其改变Slider.Value属性而不是TextBlock.FontSize属性。


现在改变文本框中的内容会在滑竿中触发一个改变,并为文本应用新的字体。同样,只有使用双向绑定这种方法才能起作用。
最后,可以交换滑竿和文本框的角色,从而将滑竿绑定到文本框。为此,需要创建一个未绑定的文本框并设置其名称:


然后可以绑定Slider.Value属性,如下所示:
Minimum="1" Maximum="40"
Value="{Binding ElementName=txtFontSize, Path=Text, Mode=TwoWay}"
TickFrequency="1" TickPlacement="TopLeft">

现在滑竿被控制。当第一次显示窗口时,检索TextBox.Text属性并使用该属性值设置滑竿的Value属性。当用户将滑竿上的滑块拖动到一个新位置时,使用绑定更新文本框。或者,用户可以通过在文本框中输入内容来更新滑竿的值(以及示例文本的字体尺寸)。
注意:
如果绑定Slider.Value属性,和前面的两个示例相比文本框的行为稍微有些不同。在文本框中的任何编辑都会立即被应用,而不是等到文本框失去焦点之后才被应用。在16.1.5节中将会学习更多与控制更新相关的内容。
正如该示例所演示的,双向绑定提供了非常大的灵活性。可以使用它们从源向目标以及从目标向源应用改变。还可以通过组合应用它们创建非常复杂的不需要编写代码的窗口。
通常,在何处放置绑定表达式是由编码模型的逻辑决定的。在前面的示例中,在TextBox.Text属性而不是在Slider.Value属性中放置绑定更合理,因为文本框是为了完成示例而添加的可选的附加内容,不是滑竿依赖的核心组件。直接将文本框绑定到TextBlock.FontSize属性而不是绑定到Slider.Value属性更加合理(从概念上讲,我们仅对当前的字体尺寸感兴趣,并且滑竿只是设置这一字体尺寸的一种方式。尽管滑竿的位置和字体尺寸相同,但如果正在试图编写尽可能清晰的标记,这一额外的细节并不是必需的)。当然,这些决定是主观的并且与编码的风格有关。最重要的是,所有这三种方法都能得到相同的行为。
在后续的几小节中,将会研究这个示例所依赖的两个细节。首先,将会分析设置绑定方向的选择。然后,将会查看在双向绑定中,当需要更新源属性时,如何才能正确地通知WPF。
16.1.4  绑定方向
到目前为止,已经介绍了单向和双向数据绑定。实际上,当设置Binding.Mode属性时,WPF允许使用5个System.Windows.Data.BindingMode枚举值中的任何一个。表16-1列出了这些枚举值。
表16-1  BindingMode枚举值
名    称
描    述
OneWay
当源属性变化时更新目标属性
TwoWay
当源属性变化时更新目标属性,并且当目标属性变化时更新源属性
OneTime
最初根据源属性值设置目标属性。然而,在此之后的所有改变都会被忽略(除非绑定被设置到一个完全不同的对象或者调用BindingExpression.UpdateTarget( )方法,如在本章后面所介绍的那样)。通常,如果知道源属性不会变化,可以使用这种模式降低开销
OneWayToSource
和OnWay类型类似,但是方向相反。当目标属性变化时更新源属性(这看起来有点像向后传递),但是目标属性永远不会被更新
Default
这种类型的绑定依赖于目标属性。它既可以是双向的(对于用户可以设置的属性,如TextBox.Text属性),也可以是单向的(对于所有其他属性)。除非明确指定了另外一种模式,否则所有的绑定都使用该模式
图16-5显示了它们之间的区别。在前面已经介绍了OneWay模式和TwoWay模式。OneTime模式非常简单。下面对其他两种选择再进行一些分析。
图16-5  绑定两个属性的不同方向
1. OneWayToSource模式
您可能会好奇为什么既有OneWay模式还有OneWayToSource模式—— 毕竟,这两种模式都创建一个以相同的方式工作的单向绑定。唯一的区别是绑定表达式放置的位置。本质上,OneWayToSource模式允许通过在原来被看作是绑定源的对象中放置绑定表达式,从而翻转源和目标。
使用这一技巧最常见的原因是要设置一个非依赖项属性的属性。在本章的开头已经学习过,绑定表达式只能被用于设置依赖项属性。但是通过使用OneWayToSource模式,可以克服这一限制,其前提是提供数值的属性本身是依赖项属性。
当进行从元素到元素的绑定时,这一技术不是很常用,因为几乎所有的元素属性都是依赖项属性。一个例外是设置可以用于构建文档(将在第19章中介绍)的内部元素。例如,分析下面的标记,该标记创建了一个FlowDocument对象,该对象对于显示静态内容中已被很好格式化的区域是很完美的:


This is a paragraph one.
This is paragraph two.


FlowDocument对象被放置到一个能够滚动的包容器中(这是可以使用的几个包容器中唯一一个能够滚动的包容器),并且提供了两个具有少量文本的段落。
现在分析如果将段落中的一些文本绑定到另外一个属性会发生什么情况。第一步是将希望改变的文本包装到一个Run对象中,Run对象代表FlowDocument对象中任意一小块文本。下一步可以试图使用一个绑定表达式设置Run对象的文本。


This is a paragraph one.

Name="runParagraphTwo">



在这个示例中,Run元素试图从一个名称为txtParagraph的文本框中提取文本。但这段代码不能正常运行,因为Run.Text属性不是依赖项属性,所以它不知道如何使用绑定表达式。解决方法是从Run元素中删除绑定表达式,而并将绑定表达式放置到文本框中:
Content for second paragraph:
Text="{Binding ElementName=runParagraphTwo, Path=Text,
Mode=OneWayToSource}">

现在,文本能够自动从文本框复制到Run元素中。当然,也可以在文本框中使用双向绑定,但是这会增加少量额外的开销。如果在Run元素中有一些初始化文本并且希望将这些初始化文本显示在绑定的文本框中,这可能是最佳方法。
2. Default模式
最初,除非显式指定其他选择,否则可能会认为所有的绑定都是单向的,这看起来好像是符合逻辑的(毕竟,简单的滑竿示例使用的就是这种方式)。然而,情况并非如此。为了演示这一事实,返回到具有能够改变字体尺寸的绑定文本框的示例。如果删除了Mode=TwoWay设置,这个示例仍然工作的很好。这是因为WPF使用了一个不同的、默认情况下依赖于所绑定属性的模式(从技术上讲,在每个依赖项属性中都具有一个元数据——FrameworkPropertyMetadata. BindsTwoWayByDefault标志—— 该标志指示属性是使用单向绑定还是双向绑定)。
通常,默认的绑定模式也正是所期望的模式。但可以设想一个示例,该示例具有一个只读的不允许用户改变的文本框。对于这种情况,通过将模式设置为单向绑定可以稍微降低一些开销。
作为首要的通用规则,显式设置绑定模式并非一个坏主意。即使在文本框示例中,也值得通过包含Mode属性来强调希望使用双向绑定。
16.1.5  绑定更新
在图16-4显示的示例中(该示例将TextBox.Text属性绑定到TextBlock.FontSize属性),还存在另外一个问题。当通过在文本框中输入内容改变显示的字体尺寸时,什么事情也不会发生。直到使用tab键将焦点转移到另外一个控件时,才会应用改变。这一行为和在滑竿控件中介绍的行为不同。在滑竿控件示例中,当拖动滑竿上的滑块时就会应用新的字体尺寸,而无需使用tab键转移焦点。
为了理解这一区别,需要深入分析这两个控件使用的绑定表达式。当使用OneWay模式或TwoWay模式的绑定时,改变之后的值会被立即从源传播到目标。对于滑竿,在TextBlock元素中有一个单向绑定表达式。因此,Slider.Value属性值的变化会被立即应用到TextBlock.FontSize属性。在文本框示例中会发生相同的行为—— 源的变化(TextBlock.FontSize属性)立即影响目标(TextBox.Text属性)。
然而,相反方向的变化传递—— 从目标到源—— 不是必须立即发生的。反而,它们的行为由Binding.UpdateSourceTrigger属性(该属性可以使用表16-2中列出的某个值)控制。当从文本框中取得文本并用于更新TextBlock.FontSize属性时,看到的正是使用UpdateSourceTrigger. LostFocus方式从目标向源更新行为的例子。
表16-2  UpdateSourceTrigger枚举值
名    称
描    述
PropertyChanged
当目标发生变化时立即更新源
LostFocus
当目标发生变化并且目标丢失焦点时更新源
Explicit
除非调用BindingExpression.UpdateSource( )方法,否则不会更新源
Default
根据目标属性确定更新行为(从技术上讲,是根据FrameworkPropertyMetadata. DefaultUpdateSourceTrigger属性决定更新行为)。对于大多数属性,默认行为是PropertyChanged,尽管TextBox.Text属性的默认行为是LostFocus
请记住,在表16-2中列出的值不影响目标的更新。它们仅仅控制TwoWay模式或OneWayToSource模式的绑定中源的更新。
根据上面介绍的内容,可以改进文本示例,从而当用户在文本框中输入内容时应用字体尺寸。下面的标记演示了如何进行改进:
Name="txtFontSize">
提示:
TextBox.Text属性的默认行为是LostFocus,这仅仅是因为当用户输入内容时,文本框中的文本会不断地变化,从而会引起多次更新。根据源控件如何更新自身,PropertyChanged更新模式能够使应用程序的运行更缓慢。此外,可能会导致源对象在一次编辑完成之前重新更新自身,这会引起验证问题。
通常,UpdateSourceTrigger.Explicit更新行为是一个很好的折衷,尽管它需要编写一些代码。例如,在文本框示例中可以添加一个Apply按钮,当单击该按钮时更新字体尺寸。然后使用BindingExpression.UpdateSource( )方法触发一个立即更新。当然,这会引起另外两个问题—— 即,什么是BindingExpression对象,以及如何获取该对象。
BindingExpression对象仅仅是一个比较小的包装了两个内容的组装包:已经学习过的Binding对象(通过BindingExpression.ParentBinding属性提供)和BindingExpression所用的绑定源对象(BindingExpression.DataItem属性)。此外,BindingExpression对象为部分绑定的立即更新提供了两个方法:UpdateSource( )方法和UpdateTarget( )方法。
为了获取BindingExpression对象,需要使用GetBindingExpression( )方法,并传入一个具有绑定的目标属性,每个元素都从FrameworkElement基类继承了该方法。下面的示例根据当前文本框中的文本改变TextBlock元素的字体尺寸:
// Get the binding that's applied to the text box.
BindingExpression binding = txtFontSize.GetBindingExpression(TextBox.TextProperty);
// Update the linked source (the TextBlock).
binding.UpdateSource();
16.1.6  绑定到非元素对象
到目前为止,一直都在讨论链接两个元素的绑定。但是在数据驱动的应用程序中,更常见的情况是创建从一个非可视化对象中提取数据的绑定。唯一的需求是希望显示的信息必须存储在一个公有属性中。WPF数据绑定基础架构不能获取私有信息或公有字段。
当绑定到一个非元素对象时,需要放弃Binding.ElementName属性,反而需要使用以下属性中的一个:
●       Source。该属性是指向源对象的引用—— 换句话说,是指向提供数据的对象。
●       RelativeSource。使用一个RelateveSource对象指向源对象,RelativeSource对象允许使用以当前元素为基础的源对象引用。这是一个特殊的工具,当编写控件模板以及数据模板时这是很方便的。
●       DataContext。如果没有使用Source属性或RelativeSource属性指定一个源,WPF就从当前元素开始查找元素树。检查每个元素的DataContext属性,并使用第一个非空的DataContext属性。如果需要将同一个对象的几个属性绑定到不同的元素,DataContext属性是非常有用的,因为可以在更高层次的包容器而不是直接在目标元素上设置DataContext属性。
下面的几小节将介绍有关这三种选择的更多细节。
1. Source属性
Source属性非常简单。唯一的问题是为了进行绑定,需要具有数据对象。在后面将会看到,可以使用几种方法获取数据对象。可以从资源中提取数据对象、可以通过编写代码生成数据对象、也可以在数据提供程序的帮助下获得数据对象。
最简单的方法是将Source属性指向一些已经准备好了的静态对象。例如,可以在代码中创建一个静态对象并使用该对象。或者,可以使用来自.NET类库的组件,如下所示:

这个绑定表达式获取静态的SystemFonts.IconFontFamily属性提供的FontFamily对象(注意,为了设置Binding.Source属性,需要借助于静态标记扩展)。然后该标记扩展将Binding.Path属性设置为FontFamily.Source属性,该属性给出了字体家族的名称。结果是一行文本。在Windows Vista中,显示的是字体名称Segoe UI。
另外一个选择是绑定到一个以前作为资源创建的对象。例如,下面的标记创建一个指向Calibri字体的FontFamily对象:

Calibri

并且下面的TextBlock元素绑定到该资源:

现在将会看到文本Calibri。
2. RelativeSource属性
RelativeSource属性可以根据相对于当前目标对象的关系指向源对象。例如,可以使用RelativeSource属性将一个元素绑定到自身或者绑定到父元素(不知道在元素树中从当前元素到绑定的父元素之间有多少代)。
为了设置Binding.RelativeSource属性,需要使用RelativeSource对象。这会使语法变得更加复杂,因为除了需要创建一个Binding对象之外,还需要在其中创建一个嵌套的RelativeSource对象。一种选择是使用属性设置语法而不是使用Binding标记扩展。例如,下面的代码为TextBlock.Text属性创建了一个Binding对象。这个Binding对象使用了一个查找父窗口并显示窗口标题的RelativeSource对象。









在该示例中,RelativeSource对象使用的是FindAncestor模式,该模式通知代码查找元素树直到发现AncestorType属性指定的元素类型。
编写绑定更常用的方法是使用Binding和RelativeSource标记扩展,将其合并到一个字符串中,如下所示:


当创建RelativeSource对象时,FindAncestor模式只是以下4种选择中的一个。表16-3列出了所有的4种模式。
表16-3  RelativeSourceMode枚举值
名    称
描    述
Self
表达式绑定到同一元素的另外一个属性上(在第10章介绍了这一技术的一个示例,其中使用该技术在触发命令的控件中显示与命令相关联的文本)
FindAncestor
表达式绑定到父元素。WPF将会查找元素树直到发现期望的父元素。为了指定父元素,还必须设置AncestorType属性指示希望查找的父元素的类型。此外,还可以使用AncestorLevel属性略过发现的一定数量的特定元素。例如,当在一棵树中查找时,如果希望绑定到第三个ListBoxItem类型的元素,应当进行如下设置:AncestorType={x:Type ListBoxItem},并且AncestorLevel=3,所以会略过前两个ListBoxItem元素。默认情况下,AncestorLevel属性设置为1,并且在找到第一个匹配的元素时停止查找
PreviousData
表达式绑定到数据绑定列表中的前一个数据项。在一个列表项中会使用这种模式
TemplateParent
表达式绑定到应用模板的元素。只有当绑定位于一个控件模板或数据模板内部时,这种模式才能工作
乍一看,RelativeSource属性看起来是不必要的方法,并且会使标记变得复杂。毕竟,为什么不使用Source属性或ElementName属性直接绑定到希望使用的源呢?然而,并不总是可以使用Source属性或ElementName属性,这通常是因为源对象和目标对象的标记在不同的标记块中。当创建控件模板和数据模板时会出现这种情况。例如,如果正在构建一个改变列表项显示方式的数据模板,可能需要访问顶级的ListBox对象以读取属性。在第17章和第18章将会介绍使用RelativeSource绑定的几个例子。
3. DataContext属性
在某些情况下,会将大量的元素绑定到同一个对象。例如,分析下面的一组TextBlock元素,每个TextBlock元素都使用类似的绑定表达式提取默认图标字体不同的细节,包括它的行间距,以及第一个字体的样式和重量(这两个都是简单的正则表达式)。可以为每个TextBlock元素使用Source属性,但是这会使标记变得非常长:






对于这种情况,使用FrameworkElement.DataContext属性定义绑定源会更加清晰并且更灵活。在这个示例中,为包含所有TextBlock元素的StackPanel面板设置DataContext属性是合理的(甚至还可以在更高层次的元素上设置DataContext属性—— 例如,整个窗口—— 但是为了使意图更清晰,在尽可能小的范围中定义绑定较合理)。
可以使用和设置Binding.Source相同的方法设置DataContext属性。换句话说,可以通过使用内部对象,从一个静态属性中提取绑定源,或者从一个资源中提取绑定源,如下所示:

现在可以通过省略源信息使绑定表达式更加流线型:

当在绑定表达式中省略源信息时,WPF会检查使用绑定的元素的DataContext属性。如果它为null,WPF会继续向上在元素树中查找第一个不为null的数据上下文(最初,所有元素的DataContext属性都是null)。如果找到了一个数据上下文,就为数据绑定使用找到的数据上下文。如果没有找到,绑定表达式不会为目标属性提供任何值。
注意:
如果使用Source属性创建一个显式标识源的绑定,元素就会使用源而不会使用可能得到的数据上下文。
这个示例显示了如何创建一个基本的绑定源不是元素的数据绑定。但在真实的应用程序中使用这种技术,还需要使用几个更加复杂的技巧。在下一节中,将会学习如何使用这些技巧构建从数据库中提取信息的数据绑定,以及如何显示提取到的信息。
16.2  使用自定义对象绑定到数据库
当开发人员听到数据绑定这一术语时,他们经常会想到一个特定的应用程序—— 从数据库中提取信息,并且不需要或几乎不需要编写代码就可以将提取到的信息显示到屏幕上。
如前所述,在WPF中数据绑定是更加通用的工具。甚至应用程序一直没有连接到数据库,它仍然可以使用数据绑定自动化元素交互的方式,或者将对象模型传递到一个合适的显示位置。然而,通过分析传统的查询并更新数据库中数据表的示例,可以学习到大量有关绑定对象的细节。但是在学习这些内容之前,需要先分析这个示例使用的自定义的数据访问组件和数据对象。
16.2.1  构建数据访问组件
在专业的应用程序中,数据库代码并非被嵌入到窗口的隐藏代码类中,而是被封装到一个专门的类中。为了得到更好的组件化特征,必须能够从应用程序以及编译过的单独的DLL组件中提取这些数据访问类。当编写访问数据库的代码时,更是如此(因为这些代码通常对性能特别敏感),这是一种很好的设计而不管数据位于何处。
设计数据访问组件
不管计划如何使用数据绑定(或者不使用),数据访问代码总是应当位于单独的类中。这种方法是确保尽可能高效地维护、优化、调试以及(可选地)重用数据访问代码的唯一途径。
当创建数据类时,应当遵循下面给出的几条基本指导原则:
● 快速打开和关闭连接。在调用的每个方法中打开数据库连接,并在方法结束之前关闭连接。这样,连接就不会无意中保持打开状态。确保在合适的时间关闭数据库连接的一种方法是使用代码块。
● 实现错误处理。使用错误处理确保连接被关闭,即使已经引发了一个异常。
● 遵循无状态的设计规则。通过参数接收方法所需要的所有信息,并通过返回值返回检索到的所有数据。这样,在许多情况下可以避免复杂化(例如,如果需要创建多线程应用程序或者在一个服务器上宿主数据库组件)。
● 在一个地方保存数据库连接字符串。理想的情况是,保存在应用程序的配置文件中。
下面示例中使用的数据库组件从Store数据库中提取一个产品信息表,Store数据库是一个包含在Microsoft案例学习中的虚构的样例数据库。可以通过本章的在线示例获取安装这个数据库的脚本。图16-6显示了Store数据库中的两个数据表以及它们的模式。
图16-6  Store数据库的一部分
数据访问类非常简单—— 它只提供了一个方法,调用者通过该方法检索一条产品记录。下面是该类的基本框架:
public class StoreDB
{
// Get the connection string from the current configuration file.
private string connectionString = Properties.Settings.Default.StoreDatabase;
public Product GetProduct(int ID)
{
...
}
}
在数据库中通过一个名称为GetProduct的存储过程执行查询。连接字符串不是硬编码的,可以从这个应用程序的.config文件中的应用程序设置中检索它(为了查看或设置应用程序设置,在Solution Explorer中双击Properties节点,然后单击Settings选项卡)。
当其他窗口需要数据时,它们调用StoreDB.GetProduct( )方法检索Product对象。Product对象是一个自定义对象,该对象只有一个目的—— 表示Products表中单条记录的信息。在下一小节中将会分析该对象。
在应用程序中为了使用StoreDB类,有如下几种选择:
●       当需要访问数据库时,窗口可以随时创建StoreDB类的一个实例。
●       可以将StoreDB类中的方法改变成静态方法。
●       可以创建StoreDB类的单一实例,并通过另外一个类的静态属性使用该实例。
前两种选择是合理的,但是这两种选择都限制了灵活性。第一种选择不能用于获取在多个窗口中使用的数据对象。即使不希望立刻获取数据,为了方便以后实现,这样设计应用程序也是值得的。同样,第二种方法假定在StoreDB类中不需要保存任何特定于实例的状态。尽管这是一个好的设计原则,但是可能希望在内存中保存一些细节(如连接字符串)。如果将StoreDB类中的方法转变为静态方法,会使得访问不同的后台数据存储中的Store数据库的实例变得困难。
最终,第三种方法最灵活。这种方法通过强制所有的窗口通过一个属性保存了交换台设计。下面是通过Application类获取StoreDB实例的一个示例:
public partial class App : System.Windows.Application
{
private static StoreDB storeDB = new StoreDB();
public static StoreDB StoreDB
{
get { return storeDB; }
}
}
在本书中,我们主要关心如何将数据对象绑定到WPF元素。创建和填充这些数据对象的实际过程(以及其他实现细节,例如,StoreDB对象是否通过几个方法调用获取数据、是否使用存储过程而不是使用在线查询、当离线时是否从本地XML文件返回数据等)不是我们关注的焦点。我们只需要理解会发生什么就可以了,下面是完整的代码:
public class StoreDB
{
private string connectionString =
Properties.Settings.Default.StoreDatabase;
public Product GetProduct(int ID)
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProductByID", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@ProductID", ID);
try
{
con.Open();
SqlDataReader reader =
cmd.ExecuteReader(CommandBehavior.SingleRow);
if (reader.Read())
{
// Create a Product object that wraps the
// current record.
Product product = new Product((string)reader["ModelNumber"],
(string)reader["ModelName"],
(decimal)reader["UnitCost"],
(string)reader["Description"] ,
(string)reader["ProductImage"]);
return(product);
}
else
{
return null;
}
}
finally
{
con.Close();
}
}
}
注意:
现在,GetProduct( )方法还没有提供任何异常处理代码,所以所有的异常将会上传到调用代码。这是一种合理的设计选择,但是您可能希望在GetProduct( )方法中捕获异常,执行所需要的清理或日志操作,然后重新抛出异常,通知调用代码发生了问题。这种设计模式被称为“调用者通知(caller inform)”。
16.2.2  构建数据对象
数据对象是指计划在用户界面中显示的信息包。可以使用任何类,提供它所包含的公有属性(不支持字段和私有属性)。此外,如果希望使用这个对象进行修改(通过双向绑定),属性不能是只读的。
下面是StoreDB类使用的Product对象:
public class Product
{
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set { modelNumber = value; }
}
private string modelName;
public string ModelName
{
get { return modelName; }
set { modelName = value; }
}
private decimal unitCost;
public decimal UnitCost
{
get { return unitCost; }
set { unitCost = value; }
}
private string description;
public string Description
{
get { return description; }
set { description = value; }
}
public Product(string modelNumber, string modelName,
decimal unitCost, string description)
{
ModelNumber = modelNumber;
ModelName = modelName;
UnitCost = unitCost;
Description = description;
}
}
16.2.3  显示绑定对象
最后一步是创建Product对象的一个实例,然后将它绑定到控件上。尽管可以创建一个Product对象并将它保存为资源或者静态属性,但是另外一种方法更合理。不过,这种方法需要使用StoreDB类在运行时创建合适的对象,然后将创建的对象绑定到窗口上。
注意:
尽管不使用代码的声明式方法听起来更优美,但是有大量的原因迫使我们在数据绑定窗口中使用少量的代码。例如,如果正在查询一个数据库,您可能希望在代码中处理连接,从而可以决定如何处理异常以及如何将问题通知给调用者。
分析图16-7显示的简单窗口。该窗口允许用户提供一个产品代码,然后在窗口下部的Grid控件中显示相应的产品信息。
图16-7  查询产品
当设计这个窗口时,不需要在运行时访问提供数据的Product对象。不过,仍然可以创建没有指定数据源的绑定,只需要指定每个元素使用的Product类的属性即可。
下面是显示一个Product对象的完整标记:












Model Number:
Text="{Binding Path=ModelNumber}">
Model Name:
Text="{Binding Path=ModelName}">
Unit Cost:
Text="{Binding Path=UnitCost}">
Description:
TextWrapping="Wrap" Text="{Binding Path=Description}">

注意,上面的代码为包装所有这些细节的Grid控件设置了一个名称,从而可以在代码中控制该Grid控件并完成数据绑定。
当第一次运行这个应用程序时,不会显示信息。虽然定义了绑定,但是没有源对象。
当用户在运行过程中单击按钮时,使用StoreDB类可以获取合适的产品数据。尽管可以通过编写代码创建每个绑定,但这不是很合理(并且相对于手工设置控件,不会节省大量代码)。然而,DataContext属性提供了一种完美的快捷方式。如果为包含所有数据绑定表达式的Grid控件设置该属性,所有的绑定表达式都会通过该属性使用数据填充它们自己。
当用户单击按钮时,下面的实际事件处理代码会进行响应:
private void cmdGetProduct_Click(object sender, RoutedEventArgs e)
{
int ID;
if (Int32.TryParse(txtID.Text, out ID))
{
try
{
gridProductDetails.DataContext = App.StoreDB.GetProduct(ID);
}
catch
{
MessageBox.Show("Error contacting database.");
}
}
else
{
MessageBox.Show("Invalid ID.");
}
}
16.2.4  更新数据库
在这个示例中,若想启用数据更新功能不需要做任何额外的工作。正如在前面学习过的,在默认情况下TextBox.Text属性使用双向绑定。因此,当在文本框中编辑文本时Product对象会被修改(从技术上讲,当使用tab键将焦点转移到一个新的字段时,每个属性都会被更新,因为TextBox.Text属性默认的源更新模式是LostFocus)。
可以在任何时候向数据库提交修改。需要做的全部工作就是为StoreDB类添加一个UpdateProduct( )方法,并为窗口添加一个Update按钮。当单击Update按钮时,代码从数据上下文中获取当前Product对象,并使用该对象提交更新:
private void cmdUpdateProduct_Click(object sender, RoutedEventArgs e)
{
Product product = (Product)gridProductDetails.DataContext;
try
{
App.StoreDB.UpdateProduct(product);
}
catch
{
MessageBox.Show("Error contacting database.");
}
}
这个示例存有一个潜在的问题。当单击Update按钮时,焦点会转移到该按钮上,并且任何还未提交的编辑会被应用到Product对象。但如果将Update按钮设置为默认按钮(通过将IsDefault属性设置为true),还有另外一种可能。用户可以修改一个字段并按回车键触发更新过程,该更新过程还没有提供最后的改变。为了避免这种情况,在执行任何数据库代码之前,可以显式地强制转移焦点,如下所示:
FocusManager.SetFocusedElement(this, (Button)sender);
16.2.5  更改通知
Product绑定示例工作的很好,因为每个Product对象本质上是固定的—— 它永远不会发生变化(除了用户在一个链接的文本框中编辑文本)。
对于简单的情况,主要关注显示的内容,并让用户编辑它们,这种行为是可以接受的。然而,很容易会想到其他不同的情况,如在代码中的其他地方对Product对象进行了修改这种情况。例如,设想有一个Increase Price按钮,执行下面一行代码:
product.UnitCost *= 1.1M;
注意:
尽管可以从数据上下文中检索Product对象,但是这个示例假定将Product对象作为窗口类的一个成员变量进行存储,这样可以简化代码且只需要更少的类型匹配。
当运行上面的代码时,您将会发现尽管Product对象已经发生了变化,但是文本框中仍然保留的是原来的数值。这是因为文本框无法知道已经修改了一个值。
可以使用如下三种方法解决这个问题:
●       可以使用在第6章学习过的语法,将Product类中的所有属性都改为依赖项属性(对于这种情况,Product类必须继承自DependencyObject类)。尽管这种方法可以让WPF为我们执行相应的工作,但是依赖项属性通常用于元素—— 在窗口中具有可视化外观的类。对于像Product类这样的数据类,这种方法并不是太合适。
●       可以为每个属性引发一个事件。对于这种情况,事件必须以porpertyNameChanged的形式进行命名(如UnitCostChanged)。当属性发生变化时,需要引发这个事件。
●       可以实现System.ComponentModel.INotifyPropertyChanged接口,该接口需要一个名称为PropertyChanged的事件。无论何时属性发生变化都必须引发PropertyChanged事件,并且通过将属性名称作为字符串指示哪个属性发生了变化。当属性发生变化时仍然需要引发事件,但是不需要为每个属性定义一个单独的事件。
第一种方法依赖于WPF的依赖项属性基础架构,而第二种和第三种方法依赖于事件。通常,当创建数据对象时,会使用第三种方法。对于非元素类这是最简单的选择。
注意:
实际上,还可以使用另外一种方法。如果怀疑绑定对象已经发生了变化,并且绑定对象不支持任何正确方式的更改通知,这时可以检索BindingExpression对象(使用FrameworkElement.GetBindingExpression( )方法),并调用BindingExpression.UpdateTarget( )方法触发一个更新。显然,这是最尴尬的解决方案—— 几乎总会看到使用这种方法。
下面重新规划Product类的定义,现在Product类使用了INotifyPropertyChanged接口,并添加了实现PropertyChanged事件的代码:
public class Product : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, e);
}
}
现在只需要在所有的属性设置器中引发PropertyChanged事件即可:
private decimal unitCost;
public decimal UnitCost
{
get { return unitCost; }
set {
unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
如果在上面的示例中使用新版本的Product类,将会得到所期望的行为。当改变当前Product对象时,会立即在文本框中显示新的信息。
提示:
如果几个数值都发生了变化,可以调用OnPropertyChanged( )方法并传递一个空字符串。告诉WPF重新评估所有绑定到类的属性的绑定表达式。
16.3  绑定到对象集合
绑定到单个对象是非常直观的。但是当需要绑定到对象的集合时——如数据表中的所有产品,问题会变得更加有趣。
到目前为止,我们介绍的每个依赖项属性都支持单一值绑定,但集合绑定需要元素更加智能。在WPF中,所有这些类都继承自能够显示条目完整列表的ItemsControl类。能够支持集合数据绑定的元素包括ListBox控件、ComboBox控件以及ListView控件(以及Menu控件和用于显示层次化数据的TreeView控件)。
提示:
尽管看起来好像WPF只提供了少数几个列表控件,但是实际上可以使用这些控件以任意不同的方式显示数据。这是因为列表控件支持数据模板,通过数据模板可以完全控制数据项的显示方式。在第17章将会学习有关数据模板的更多内容。
为了支持集合绑定,ItemsControl类定义了表16-4中列出的三个关键属性。
表16-4  ItemsControl类中用于数据绑定的属性
名    称
描    述
ItemsSource
指向一个集合,该集合包含所有在列表中显示的对象
DisplayMemberPath
标识一个用于为每个项创建显示文本的属性
ItemTemplate
接受一个数据模板,用于为每个项创建可视化外观。这个属性比DisplayMemberPath属性的功能更加强大,并且在第17章中将会学习如何使用该属性
现在,您可能想知道什么类型的集合可以用于填充ItemSource属性。幸运的是,可以使用任何内容。唯一的要求是支持IEnumerable接口,数组、各种类型的集合以及许多更特殊的包装了数据项组的对象都支持该接口。然而,基本的IEnumerable接口仅支持只读的绑定。如果希望编辑集合(例如,希望向集合插入和删除元素),则需要更加复杂的架构,下面很快就会介绍这种架构。
16.3.1  显示和编辑集合元素
分析图16-8显示的窗口,该窗口显示了一个产品列表。当选择某个产品时,该产品的信息会显示在窗口底部,可以在此编辑该产品的信息(在这个示例中,通过使用GridSplitter控件来调整窗口顶部和底部的空间)。
为了创建这个示例,首先需要构建数据访问逻辑。在本例中,StoreDB.GetProducts( )方法使用GetProducts存储过程来检索数据库中所有产品的列表。为每条记录创建一个Product对象,并将创建的对象添加到通用的List集合中(在此可以使用任何集合—— 例如,数组或具有相同功能的弱类型的ArrayList集合)。
图16-8  产品列表
下面是GetProducts( )方法的实现代码:
public List GetProducts()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProducts", con);
cmd.CommandType = CommandType.StoredProcedure;
List products = new List();
try
{
con.Open();
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
// Create a Product object that wraps the
// current record.
Product product = new Product(
(string)reader["ModelNumber"],
(string)reader["ModelName"],
(decimal)reader["UnitCost"],
(string)reader["Description"],
(string)reader["CategoryName"],
(string)reader["ProductImage"] );
// Add to collection
products.Add(product);
}
}
finally
{
con.Close();
}
return products;
}
当单击Get Products按钮时,事件处理代码调用GetProducts( )方法并将返回结果作为列表的ItemsSource属性的值。为方便在代码中进行访问,还将该集合保存为窗口类的一个成员变量。
private List products;
private void cmdGetProducts_Click(object sender, RoutedEventArgs e)
{
products = App.StoreDB.GetProducts();
lstProducts.ItemsSource = products;
}
上面的代码会成功地使用Product对象填充列表。因为列表不知道如何显示产品对象,所以它只是调用ToString( )方法。因为Product类没有重写该方法,所以只是为每个项显示其完全限定的类名(见图16-9)。
图16-9  没有多大意义的绑定列表
有三种选择可以解决这一问题:
●       设置列表的DisplayMemberPath属性。例如,将该属性设置为ModelName以得到图16-9所显示的结果。
●       重写ToString( )方法,返回更有用的信息。例如,可以为每个项返回一个包含型号编号和型号名称的字符串。通过这种方法可以显示比属性更多的信息(例如,在一个Customer类中组合FirstName属性和SecondName属性是非常合适的)。然而,仍然不能对数据的显示进行更多的控制。
●       提供数据模板。使用这种方法可以显示属性值的任何排列(并且同时显示固定的文本)。在第17章中将会学习如何使用这一技巧。
一旦决定了如何在列表中显示信息,就已经准备好了解决第二个挑战:在列表下面的网格中为当前选择的项显示细节。可以通过响应SelectionChanged事件并手工改变数据上下文来完成这一挑战,但是还有更快的不需要编写任何代码的方法。这种方法只需要简单地为Grid.DataContext属性设置绑定表达式,从列表中提取选择的Product对象,如下所示:

...

当第一次显示窗口时,在列表中没有选择任何内容。ListBox.SelectedItem属性为null,所以Grid.DataContext属性也为null,从而不会显示任何信息。一旦选择了某个项,数据上下文就会被设置为相应的对象,从而会显示所有的信息。
如果测试该示例,就会惊奇地发现它已经具有了全部的功能。可以编辑产品项,导航到产品项(使用列表),然后返回就会看到编辑信息已经成功地被提交了。实际上,甚至可以改变影响列表中显示文本的值。如果修改型号名称并使用tab键将焦点转移到其他控件,列表中的相应产品项就会被立即更新(有经验的开发人员会发现这个功能是Windows窗体应用程序所不具备的)。
提示:
为了阻止一个字段被编辑,可以将文本框的IsLocked属性设置为true,更好的方法是使用只读的控件,如TextBlock。
主从模式显示
前面已经介绍过,可以将其他元素绑定到列表的SelectedItem属性,以显示更多与当前选择的产品项相关的细节。有趣的是,可以使用相同的技术构建数据的主从模式显示。例如,可以创建一个显示一系列目录和一系列产品的窗口。当用户在目录列表中选择一个目录时,可以在第二个列表中显示只属于当前目录的产品。
为了实现这种效果,需要一个父数据对象,该父数据对象通过一个属性提供相关的子数据对象集合。例如,可以构建一个Category类,该类具有一个被命名为Category.Products的属性,该属性包含的产品属于该目录(实际上,在第18章可以发现这样的Category类)。然后可以构建一个具有两个列表的主从模式显示。使用Category对象填充第一个列表。为了显示相关的产品,将第二个列表—— 该列表显示产品—— 绑定到第一个列表的SelectedItem.Products属性,这会告诉第二个列表获取当前Category对象,并提取链接的Product对象的集合,并显示链接的Product对象。
在第18章会介绍一个使用相关数据的示例,该示例使用TreeView控件显示产品的目录列表。您将会看到这个示例的两个版本—— 一个版本使用Category对象和Product对象,另一个版本使用ADO.NET的DataRelation对象。
当然,为了完成这个示例,从应用程序的角度来分析需要提供一些代码。例如,可能需要一个UpdateProducts( )方法,该方法接收集合或产品,并执行相应的语句。因为普通的.NET对象没有提供任何变化跟踪,所以可能考虑使用ADO.NET的DataSet对象(在本章稍后会介绍该对象)。或者,可能希望强制用户同时更新所有的记录(一个选择是当文本框中的文本被修改后禁用列表,然后通过单击Cancel按钮强制用户取消改变,或者通过单击Update按钮立即更新改变)。
16.3.2  插入和移除集合元素
前面示例的一个限制是不能获取对集合进行的改变。虽然该示例注意到了发生变化的Product对象,但是如果通过代码添加了一个新的项或删除了一个项时并不能更新列表。
例如,假设添加一个Delete按钮,执行下面的代码:
private void cmdDeleteProduct_Click(object sender, RoutedEventArgs e)
{
products.Remove((Product)lstProducts.SelectedItem);
}
虽然删除的项已从集合中移除了,但是它仍然保留在绑定列表中。
为了启用集合更改跟踪,需要使用一个实现了INotifyCollectionChanged接口的集合。大多数通用集合没有实现该接口,包括在当前示例中使用的List集合。实际上,WPF提供了一个使用INotifyCollectionChanged接口的集合:ObservableCollection类。
注意:
如果有一个从Windows窗体导入的对象模型,可以使用Windows窗体中与Observable Collection类等效的BindingList集合。BindingList集合实现了IBindingList接口,而不是INotify CollectionChanged接口,该接口包含的ListChanged事件与INotifyCollectionChanged.Collection Changed事件扮演相同的角色。此外,可以继承BindingList类以得到附加的用于排序以及在Windows窗体的DataGridView控件中创建项的功能。
可以从ObservableCollection类继承一个自定义集合,从而可以自定义它的工作方式,但是这不是必需的。在当前的示例中,使用ObservableCollection集合代替List集合就已经足够了,如下所示:
public List GetProducts()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProducts", con);
cmd.CommandType = CommandType.StoredProcedure;
ObservableCollection products = new ObservableCollection();
...
GetProducts()方法的返回类型可以是List,因为ObservableCollection类继承自List类。为了让该示例更加通用,可以为返回类型使用ICollection集合,因为ICollection接口包含了所有需要使用的成员。
现在,如果通过代码删除或添加项,列表就会相应地进行更新。当然,仍然需要创建在集合被修改之前执行的数据访问代码—— 例如,从后台数据库中删除产品记录的代码。
16.3.3  绑定到ADO.NET对象
前面学习过的自定义对象的所有功能,对处理ADO.NET断开链接的数据对象同样有效。
例如,可以创建在图16-9中所看到的相同的用户界面,但是在后台使用的是DataSet、DataTable以及DataRow对象,而不是使用自定义的Product类和ObservableCollection类。
为了测试这种情况,首先分析以下版本的GetProducts( )方法,该方法提取相同的数据,但是将数据打包到一个DataTable对象中:
public DataTable GetProducts()
{
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand("GetProducts", con);
cmd.CommandType = CommandType.StoredProcedure;
SqlDataAdapter adapter = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
adapter.Fill(ds, "Products");
return ds.Tables[0];
}
可以检索这个DataTable对象并使用与绑定ObservableCollection集合相同的方式将它绑定到列表上。唯一的区别是不能直接绑定到DataTable对象本身。而是需要通过一个众所周知的DataView对象。尽管可以手工创建DataView对象,但是每个DataTable对象都包含一个已经准备好了的DataView对象,可以通过DataTable.DefaultView属性获取该对象。
注意:
这并不是什么新的限制。即使在Windows窗体应用程序中,所有的DataTable数据绑定也是通过DataView进行的。区别是在Windows窗体中可以隐藏这一实际过程。它允许用户编写看起来好像是直接绑定到DataTable对象的代码,而当代码实际运行时使用的却是DataTable. DefaultView属性提供的DataView对象。
下面是所需要的代码:
private DataTable products;
private void cmdGetProducts_Click(object sender, RoutedEventArgs e)
{
products = App.StoreDB.GetProducts();
lstProducts.ItemsSource = products.DefaultView;
}
现在列表会为DataTable.Rows集合中的每个DataRow对象创建一个单独的项。为了决定在列表中显示什么内容,需要使用希望显示的字段的名称,或者使用数据模板(将在第17章介绍相关内容)设置DisplayMemberPath属性。
这个示例好的一面在于一旦改变了提取数据的代码,就不需要进行任何其他修改。当在列表中选择一个项时,下面的Grid控件会为它的数据上下文提取所选择的项。用于ProductList集合的标记仍然能够工作,因为Product类的属性名称和DataRow对象的字段名称是一致的。
该示例另外一个好的方面在于,若要实现更改通知不需要采取任何额外的步骤。这是因为DataView类实现了IBindingList接口,如果添加了一个新的DataRow对象或者删除了一个已经存在的DataRow对象,DataView类会通知WPF基础架构。
然而,当删除DataRow对象时需要小心。可能会使用类似下面的代码删除当前选择的记录:
products.Rows.Remove((DataRow)lstProducts.SelectedItem);
上面的代码在两个方面存在问题。首先,在列表中所选的项不是DataRow对象—— 而是更轻量级的由DataView对象提供的DataRowView包装器。其次,您可能不希望从数据表的行集合中删除DataRow对象,而是希望将它标记为已经删除,从而当向数据库提交修改时,删除相应的记录。
下面是正确的代码,该代码获取选择的DataRowView对象,使用该对象的Row属性查找对应的DataRow对象,并调用查找到的DataRow对象的Delete( )方法将行标识为已经被删除。
((DataRowView)lstProducts.SelectedItem).Row.Delete();
现在,计划被删除的DataRow对象从列表中消失了,尽管从技术上讲它仍然位于DataTable.Rows集合中。这是因为DataView中的默认过滤设置隐藏了所有已删除的记录。在第17章将会学习更多有关过滤的内容。
16.3.4  绑定到LINQ表达式
使用.NET 3.5而不使用.NET 3.0的一个关键原因是,.NET 3.5支持语言集成查询(Language Integrated Query,LINQ),LINQ是专门用于查询的语法,它能够查询各种数据源,并且与C#紧密集成。LINQ可以使用具有LINQ提供者的任何数据源。通过.NET 3.5提供的支持,可以使用熟悉的结构化的LINQ查询,从位于内存中的集合、XML文件或者SQL Server数据库中检索数据,并对检索到的数据进行变换。
尽管LINQ在一定程度上超出了本章研究的范围,但是可以通过一个简单的示例学习许多内容。例如,设想有一个集合products,并且希望创建第二个集合,在该集合中只包含价格超过100美元的Product对象,可以编写如下代码:
// Get the full list of products.
List products = App.StoreDB.GetProducts();
// Create a second collection with matching products.
List matches = new List();
foreach (Product product in products)
{
if (product.UnitCost >= 100)
{
matches.Add(product);
}
}
若使用LINQ,就可以使用下面的表达式,该表达式更加简明:
// Get the full list of products.
List products = App.StoreDB.GetProducts();
// Create a second collection with matching products.
IEnumerable matches = from product in products
where product.UnitCost >= 100
select product;
这个示例为集合使用了LINQ,这意味着它使用LINQ表达式从一个位于内存中的集合查询数据。LINQ表达式使用的是一套新的语言关键字,包括from、in、where以及select。这些LINQ关键字真正是C#语言的一部分。
注意:
完整讨论LINQ超出了本书的范围(有关LINQ的细节,可以参阅http://msdn. microsoft.com/ data/ref/linq上的LINQ开发中心或者http://msdn2.microsoft.com/en-us /vcsharp/aa336746.aspx上的大量的LINQ示例)。
LINQ使用的是IEnumerable接口。不管使用什么数据源,每个LINQ表达式都返回一些实现了IEnumerable接口的对象。因为IEnumerable接口扩展了IEnumerable接口,所以可以将它绑定到WPF窗口,就像绑定一个普通集合一样:
lstProducts.ItemsSource = matches;
也就是说,有几个问题值得考虑。下面的几部分给出了相关细节。
1. 将IEnumerable接口转换为普通集合
与ObservableCollection集合以及DataTable类不同,IEnumerable接口没有提供添加和删除项的方法。如果需要使用这一功能,首先需要使用ToArray( )方法或ToList( )方法将IEnumerable对象转换为一个数组或List集合。
下面的示例使用ToList( )方法将(在前面所显示的)LINQ查询的结果转换成强类型的Product对象的List集合:
List productMatches = matches.ToList();
注意:
ToList( )是一个扩展方法,这意味着定义它的类并不是使用它的类。从技术上讲,ToList()方法是在System.Linq.Enumerable帮助类中定义的,并且所有的IEnumerable对象都可以使用该方法。然而,如果Enumerable类超出了范围,就不能使用该方法,这意味着如果没有导入System.Linq名称空间,在此给出的代码就不能正常运行。
ToList()方法导致LINQ表达式被立即评估。最终得到的结果是一个普通的集合,可以使用各种常见的方式处理该集合。例如,可以在一个ObservableCollection集合中包装该集合,以获得通知事件,从而所有的变化都能够被立即反映到控件上:
ObservableCollection productMatchesTracked =
new ObservableCollection(productMatches);
然后,可以将productMatchesTracked集合绑定到窗口中的控件上。
2. 延迟执行
LINQ使用的是延迟执行。这可能和您所期望的不同,LINQ表达式的结果(如上面示例中的matches对象)不是一个简单的集合,而是一个特殊的LINQ对象,该对象能够在需要时而不是当创建LINQ表达式时提取数据。
在这个示例中,matches对象是WhereIterator类的一个实例,而WhereIterator类是嵌套于System.Linq.Enumerable类中的私有类:
matches = from product in products
where product.UnitCost >= 100
select product;
根据使用的特定查询,LINQ表达式可能返回一个不同的对象。例如,合并来自两个不同的集合数据的联合表达式,可能返回一个UnionIterator私有类的实例。或者,如果通过删除where子句简化查询,会返回一个简单的SelectIterator类的实例。实际上不需要知道代码使用的是哪个特定迭代类的实例,因为是通过IEnumerable接口与结果进行交互的(但是如果您非常好奇,在中断模式下运行时,可以在Visual Studio中将鼠标停留在恰当变量的上面以查看对象的类型)。
LINQ迭代器对象在定义LINQ表达式和执行LINQ表达式之间添加了额外的一层。只要迭代一个LINQ迭代器,如WhereIterator,就会返回所需要的数据。例如,如果编写一个foreach代码块遍历matches集合,这一动作会强制LINQ表达式被重新评估。当将IEnumerable对象绑定到WPF窗口上时会发生相同的事情,在这种情况下,WPF数据绑定基础架构会迭代它的内容。
注意:
LINQ需要使用延迟执行其实并没有技术上的原因,但是有许多理由能够说明这是一个好方法。在许多情况下,它允许LINQ使用性能优化技术,如果不使用延迟执行,这些优化是不可能实现的。例如,当使用具有LINQ to SQL的数据库关系时,可以避免加载实际上不使用的数据。当创建需要使用其他LINQ查询的LINQ查询时,延迟执行也允许LINQ使用优化技术。
延迟执行与LINQ TO SQL
当使用可能不可用的数据源时,理解延迟执行是很重要的。在到目前为止所介绍的示例中,LINQ表达式对内存中的集合进行操作,所以(至少对于应用程序开发人员来说)确切知道何时评估表达式并不是很重要。然而,当使用LINQ to SQL针对数据库执行即时查询时,情况就不同了。对于这种情况,枚举IEnumerable结果对象会使.NET建立一个数据库连接并且执行查询。 这显然是一个冒险的动作—— 如果数据库服务器不可用或者不能响应,那么就会发生不可预测的异常。为此,通常以两种受限的方式使用LINQ表达式。
● 在(使用普通的数据访问代码)检索到数据之后,使用LINQ从集合中过滤结果。如果需要为同一个结果集提供各种不同的视图,这是很方便的。这正是在本节中(以及在本章后面的示例中)演示的方法。
● 使用LINQ to SQL获取需要的数据。这样可以避免编写低级的数据访问代码。使用ToList( )方法强制查询立即执行,并返回一个普通的集合。
使用LINQ to SQL创建数据库组件,并且从数据库查询返回IEnumerable结果,这通常不是一个好方法。因为这样就不能控制何时执行查询,也不能控制如何处理潜在的错误(还不能控制进行查询的次数,因为每次迭代集合或者将其绑定到控件上时,都会重新评估LINQ表达式。将相同的数据绑定到几个不同的控件,只会为数据库服务器增加不必要的额外工作)。
LINQ to SQL是LINQ的一个重要主题。它提供了灵活性,是一种从数据库中提取数据并将数据放置到已经设计好的自定义对象中的不需要使用SQL的方法(代价是需要学习LINQ语法以及另外一种数据访问模型)。目前,LINQ to SQL只支持SQL Server数据库。如果对测试LINQ to SQL感兴趣,可以通过网站http://msdn2.microsoft.com/en-us/library/ bb425822.aspx学习LINQ的详细内容,或者考虑购买一本专门的LINQ书籍,如Pro LINQ(该书已由清华大学出版社引进并出版,中文书名为《LINQ高级编程》),开始学习LINQ的详细内容。
16.4  数据转换
在普通的绑定中,从源到目标的信息在传递过程中没有任何变化。这看起来是符合逻辑的,但是我们并不总是希望出现这种行为。通常,数据源使用的是低级的表达方式,我们可能不希望直接在用户界面中使用这种低级的表达方式。例如,可能希望使用数字编码代替更易读的字符串,数字需要被削减到合适的尺寸,日期需要使用长格式显示等。如果这样的话,就需要有一种方法将这些数值转换为恰当的显示形式。并且如果正在使用双向绑定,还需要进行反向转换—— 获取用户提供的数据并将它们转换到适合于在恰当的数据对象中保存的表达形式。
幸运的是,WPF允许通过创建(并使用)值转换器类完成这些工作。在显示目标之前,值转换器负责转换源数据,并且(对于双向绑定)在将新的目标值应用回源之前转换新的目标值。
注意:
这种转换方法类似于在Windows窗体中使用Format和Parse绑定事件进行数据绑定的方法。区别是在Windows窗体应用程序中,可以在任何地方编写代码逻辑—— 只需要将这两个事件关联到绑定。在WPF中,这一逻辑必须被封装到一个值转换器类中,从而使重用更加容易。
值转换器是WPF数据绑定难题中非常有用的一部分。可以通过如下几种有用的方式使用它们:
●       将数据格式化为字符串表示形式。例如,可以将一个数字转换成一个货币字符串。这是值转换器最明显的用途,但不是唯一的用途。
●       创建特定类型的WPF对象。例如,可以读取一块二进制数据,并创建一幅能够绑定到Image元素的BitmapImage对象。
●       根据绑定数据有条件地改变元素中的属性。例如,可以创建一个值转换器,在一个特定的范围内改变元素的背景色。
在后续的几小节中,将会分析使用这些方法的示例。
16.4.1  使用值转换器格式化字符串
对于格式化需要显示为文本的数字,使用值转换器是非常完美的。例如,考虑前面示例中的Product.UnitCost属性。该属性以小数的形式存储,因此,当在文本框中显示该属性时,将会看到类似3.9900的内容。这种显示格式不但显示了比看起来可能更多的小数部分,而且缺少货币符号。更直观的表达方式应当是显示货币格式的数值$3.99,如图16-10中所显示的那样。
要创建值转换器,需要执行以下4个步骤:
(1) 创建一个实现IValueConverter接口的类。
(2) 为该类的声明添加ValueConversion特性,并指定目标数据类型。
(3) 实现Convert( )方法,该方法将数据从它原来的格式转换为显示格式。
图16-10  显示格式化的货币值
(4) 实现ConvertBack( )方法,该方法执行相反的变换,将值从显示格式转换为原来的格式。
图16-11显示了值转换器的工作原理。
图16-11  转换绑定的数据
从小数转换到货币时,可以使用Decimal.ToString( )方法获取所期望的格式化的字符串表示。只需要简单地指定货币格式化字符串“C”即可,如下所示:
string currencyText = decimalPrice.ToString("C");
上面的代码使用了应用到当前线程的文化设置。在配置为英语(美国)区域的计算机上运行使用en-US本地化的程序,并显示美元货币符号($)。在配置为其他区域的计算机上运行时会显示不同的货币符号。如果这不是所期望的结果(例如,总是希望显示美元符号),可以使用重载版本的ToString( )方法指定文化,如下所示:
CultureInfo culture = new CultureInfo("en-US");
string currencyText = decimalPrice.ToString("C", culture);
在Visual Studio帮助文档中可以学习到所有的格式字符串。表16-5和表16-6显示了一些最常用的选项,这些选项分别用于转换数字和日期数值。
表16-5  数字数据格式字符串
类    型
格式字符串
示    例
货币
C
$1 234.50
圆括号表示负值:($1 234.50)。
货币符号特定于区域
科学计数法(指数)
E
1 234.50E+004
百分数
P
45.6%
固定小数
F?
依赖于设置的小数位数。F3格式化数值类似于123.400。F0格式化数值类似于123
表16-6  时间和日期格式字符串
类    型
格式字符串
格    式
短日期
d
M/d/yyyy
例如:01/30/2008
长日期
D
dddd,MMMM dd,yyyy
例如:星期三,一月 30,2008
长日期和短时间
f
dddd,MMMM dd,yyyy HH:mm aa
例如:星期三,一月 30,2008 10:00 AM
(续表)
类    型
格式字符串
格    式
长日期和长时间
F
dddd,MMMM dd,yyyy HH:mm:ss aa
例如:星期三,一月 30,2008  10:00:23 AM
ISO Sortable标准
s
yyyy-MM-dd HH:mm:ss
例如:2008-01-30  10:00:23
月和日
M
MMMM dd
例如:一月 30
通用格式
G
M/d/yyyy HH:mm:ss aa
(依赖于特定的区域设置)
例如:10/30/2008  10:00:23 AM
从显示格式转换回希望的数值更麻烦一些。Decimal类型的Parse( )方法和TryParse( )方法是执行该工作最合逻辑的选择,但是通常它们不能处理包含了货币符号的字符串。解决方法是使用重载版本的接受System.Globalization.NumberStyles数值的Parse( )方法或TryParse( )方法。如果提供了NumberStyles.Any值,如果存在货币符号,就能够成功地略过货币符号。
下面是用于处理类似于Product.UnitCost属性的价格数值的值转换器的完整代码:
[ValueConversion(typeof(decimal), typeof(string))]
public class PriceConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
decimal price = (decimal)value;
return price.ToString("C", culture);
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
string price = value.ToString(culture);
decimal result;
if (Decimal.TryParse(price, NumberStyles.Any, culture, out result))
{
return result;
}
return value;
}
}
为了使用这个值转换器,首先需要将项目的名称空间映射到一个能够在标记中使用的XML名称空间前缀。下面的示例使用了local名称空间前缀,并假定值转换器位于DataBinding名称空间中:
xmlns:local="clr-namespace:DataBinding"
典型的情况是,将会为包含所有标记的标签添加这个特性。
现在,只需要简单地创建PriceConverter类的一个实例,并将该实例指定给绑定的Converter属性。为此,需要使用如下所示的更长的语法:
Unit Cost:









在许多情况下,同一个转换器可以被用于多个绑定。对于这种情况,为每个绑定都创建一个转换器实例是不合理的,而应当在Resources集合中创建一个转换器对象,如下所示:



之后,可以在绑定中使用StaticResource引用指向资源中的转换器对象,正如在第11章中所描述的那样:
Text={Binding Path=UnitCost, Converter={StaticResource PriceConverter}">

16.4.2  使用值转换器创建对象
当需要跨越数据在自定义类中存储的方式和在窗口中显示的方式之间的鸿沟时,值转换器是必不可少的。例如,设想有一个在数据库的字段中存储为二进制数据的图片。可以将二进制数据转换为System.Windows.Media.Imaging.BitmapImage对象,并保存为自定义数据对象的一部分。然而,这种设计可能不合适。
例如,可能需要灵活地创建多个图像的对象表示,因为在WPF应用程序和Windows窗体应用程序(需要使用System.Drawing.Bitmap类)中都要使用自定义的数据。对于这种情况,在数据对象中存储原始的二进制数据并使用值转换器将它转换成WPF的BitmapImage对象更合理(为了将图像绑定到Windows窗体应用程序,可以使用System.Windows.Binding类的Format事件和Parse事件)。
提示:
要将一块二进制数据转换成一幅图像,首先必须创建一个BitmapImage对象,并将图像数据读入到一个MemoryStream对象中。然后,可以调用BitmapImage.BeginInit( )方法,设置BitmapImage对象的StreamSource属性,使其指向MemoryStream对象,并调用EndInit( )方法完成图像加载。
Store数据库中的Products数据表不包含二进制图片数据,但它包含了一个存储与产品图像相关联的文件名的ProductImage字段。对于这种情况,更应当延迟创建图像对象。首先,根据应用程序运行的位置图像可能不可得。其次,除非图像即将显示,否则使用额外的内存保存图像是没有意义的。
ProductImage字段提供了文件名称,但不是图像文件的完整路径,这样可以灵活地将图像文件放置到任何合适的位置。值转换器负责根据ProductImage字段和希望使用的目录创建指向图像文件的URI。使用自定义的ImageDirectory属性保存目录,该属性默认保存的是当前目录。
下面是执行转换的ImagePathConverter类的完整代码:
public class ImagePathConverter : IValueConverter
{
private string imageDirectory = Directory.GetCurrentDirectory();
public string ImageDirectory
{
get { return imageDirectory; }
set { imageDirectory = value; }
}
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
string imagePath = Path.Combine(ImageDirectory, (string)value);
return new BitmapImage(new Uri(imagePath));
}
public object ConvertBack(object value, Type targetType, object
parameter,System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
要使用这个类型转换器,首先需要将它添加到资源。在这个示例中,没有设置ImageDirectory属性,这意味着ImagePathConverter对象默认使用应用程序的当前目录:




现在可以很容易地创建使用这个值转换器的绑定表达式:
HorizontalAlignment="Left"
Source="{Binding Path=ProductImagePath,
Converter={StaticResource ImagePathConverter}}">

上面的标记可以工作,因为Image.Source属性期望一个ImageSource对象,并且BitmapImage类继承自ImageSource类。
图16-12显示了结果。
图16-12  显示绑定的图像
可以通过两种方法改进这个示例。首先,当试图为一个不存在的文件创建BitmapImage对象时引发一个异常,当设置DataContext属性、ItemsSource属性或Source属性时,接收该异常。此外,可以为ImagePathConverter类添加用于配置这一行为的属性。例如,可以添加一个Boolean类型的SuppressException属性。如果该属性设置为true,就可以在Convert( )方法中捕获异常,然后返回Binding.DoNothing值(这会通知WPF暂时认为没有设置数据绑定)。或者,还可以添加一个能够保存BitmapImage对象的DefaultImage属性。如果发生异常,ImagePathConverter类可以返回一个默认图像。
您可能还会注意到,这个转换器只支持单向转换。这是因为不可能改变BitmapImage对象,并使用它更新图像路径。但也可以采取另外一种方法,该方法并不从ImagePathConverter返回BitmapImage对象,而是从Convert( )方法简单地返回完全限定的URI,如下所示:
return new Uri(imagePath);
上面的代码同样能够正常运行,因为Image元素使用类型转换器将Uri转换为它实际所希望的ImageSource对象。如果采用这种方法,就可以让用户选择一个新的路径(可能使用一个通过OpenFileDialog类设置的TextBox控件)。接下来就可以在ConvertBack( )方法中提取文件名,并使用该文件名更新存储在数据对象中的图像路径。
16.4.3  应用条件格式化
那些最有趣的值转换器并不为表示数据而格式化数据,但它们会根据数据规则被用于格式化元素与可视化相关的其他方面。
例如,设想希望通过不同的背景色标志那些价格高的产品项。可以很容易地使用下面的值转换器封装这一逻辑:
public class PriceToBackgroundConverter : IValueConverter
{
public decimal MinimumPriceToHighlight
{
get; set;
}
public Brush HighlightBrush
{
get; set;
}
public Brush DefaultBrush
{
get; set;
}
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
decimal price = (decimal)value;
if (price >= MinimumPriceToHighlight)
return HighlightBrush;
else
return DefaultBrush;
}
public object ConvertBack(object value, Type targetType, object
parameter,System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
同样,为了能够重用,对值转换器进行了仔细的设计。在转换器中并没有硬编码高亮度颜色,而是在XAML中通过使用转换器的代码指定该颜色:
DefaultBrush="{x:Null}" HighlightBrush="Orange" MinimumPriceToHighlight="50">

在此使用的是画刷而不是颜色,从而可以使用渐变和背景图像创建更加高级的高亮度效果。如果希望保持标准的、透明的背景(从而使用父元素的背景),只需要将DefaultBrush属性或HighlightBursh属性设置为null即可,如下所示:
"{Binding Path=UnitCost, Converter={StaticResource PriceToBackgroundConverter}}"
... >
应用条件格式化的其他方法
使用自定义的IValueConverter接口,只是根据数据对象应用条件格式化方法中的一种。还可以使用样式中的数据触发器、样式选择器以及模板选择器,所有这些内容都将在第17章中介绍。这些方法中的每一种都有其自身的优点和缺点。
当需要在元素中根据绑定的数据对象设置一个单独的属性时,使用IValueConverter接口方法较合适。这种方法很容易,并且它能够自动保持同步。如果绑定的数据对象发生了变化,链接的属性也会被立即改变。
数据触发器同样很简单,但是它们仅支持非常简单的测试是否相等的逻辑。例如,数据触发器可以对一个特定目录中的产品应用格式化,但是它不能对价格高于一个指定的最小值的产品应用格式化。数据触发器的关键优点是,可以使用它们应用特定类型的格式化,并且不需要编写任何代码就可以选择效果。
样式选择器和模板选择器是最强大的一种方法。使用它们可以在目标元素中一次改变多个属性,并且可以改变列表项在列表中的显示方式。但它们也增加了额外的复杂性。此外,当绑定的数据发生变化时,为了重新应用样式和模板,还需要添加代码。
16.4.4  评估多个属性
可以使用值转换器实现最后一个技巧—— 评估几个不同的字段并使用它们创建一个转换后的单独数值。例如,可以使用这种方法组合不同部分的信息(如FirstName字段和LastName字段)、执行计算(例如,将UnitPrice乘以UnitsInStock),以及同时使用多个细节进行格式化(例如,在一个特定目录中高亮度显示所有价格高的产品)。
为了使用这一技巧,需要以下两个要素:
●       一个定义绑定的MultiBingding对象(而不是普通的Binding对象)
●       一个实现了IMultiValueConverter接口(而不是IValueConverter接口)的转换器
MultiBinding对象组合了一系列的Binding对象。下面示例中的MultiBinding对象使用了数据对象中的两个属性。
Total Stock Value:








IMultiValueConverter接口定义了和IValueConverter接口中类似的Convert( )方法和ConvertBack( )方法。主要的区别是提供了一个保存数值的数组而不是单个数值。这些数值在数组中使用和在标记中定义它们相同的顺序放置。因此,在上一个示例中可以首先显示UnitCost字段,然后显示UnitsInStock字段。
下面是ValueInStockConverter类的代码:
public class ValueInStockConverter : IMultiValueConverter
{
public object Convert(object[ ] values, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
// Return the total value of all the items in stock.
decimal unitCost = (decimal)values[0];
int unitsInStock = (int)values[1];
return unitCost * unitsInStock;
}
public object[ ] ConvertBack(object value, Type[ ] targetTypes,
object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
16.5.1  在数据对象中进行验证
一些开发人员直接在他们的数据对象中构建错误检查逻辑。例如,下面是Product.UnitPrice属性的修改版本,该版本不允许使用负数:
public decimal UnitCost
{
get { return unitCost; }
set
{
if (value < 0)
throw new ArgumentException("UnitCost cannot be negative.");
else
{
unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
}
这个示例中的验证逻辑防止使用负的价格数值,但是它不能为用户提供任何与问题相关的反馈。正如前面所学习过的,WPF会默默地忽略当设置和获取属性时发生的数据绑定错误。对于这种情况,用户无法知道更新已经被拒绝。实际上,非法的数值仍然保留在文本框中—— 它只是没有被应用到绑定的数据对象。为了改进这一问题,需要借助于Exception ValidationRule验证规则,在后面将介绍该验证规则。
数据对象和验证
在数据对象中放置验证逻辑是否是一种好方法,这永远是一场不会结束的辩论。
这种方法有一些优点—— 例如,它总是会捕获所有的错误,而不管这些错误是由于非法的用户编辑、编码错误引起的,还是由于根据其他非法数据进行计算引起的。然而,这种方法的缺点是会使数据对象变得更加复杂,并且将用于前端应用程序的验证代码移到了后端的数据模型中。
如果不仔细地使用这种方法,属性验证可能会无意中排除那些对数据对象非常合理的使用。它们还可能会导致不一致的且实际上是复合的数据错误(例如,UnitsInStock属性保存数值–10可能是不合理的,但是如果后台数据库存储了这个数值,可能仍然希望创建相应的数据对象,从而可以在用户界面中编辑它)。有时,这类问题可以通过创建另外一层对象来解决—— 例如,在一个复杂的系统中,开发人员可以构建一个丰富的商务对象模型,而不是简单的数据对象层。
在当前的示例中,StoreDB类和Product类都是作为后端数据访问组件设计的,Product类是一个完美的数据包,可以将信息从一个代码层传递到另外一个代码层。因此,验证代码被放置在Product类中。
1. ExceptionValidationRule验证规则
ExceptionValidationRule是预先构建的验证规则,它向WPF报告所有的异常。要使用ExceptionValidationRule验证规则,必须将它添加到Binding.ValidationRules集合中,如下所示:












这个示例同时使用了值转换器和验证规则。通常是在转换值之前执行验证,但ExceptionValidationRule验证规则是一个例外。它捕获在任何位置发生的异常,包括当编辑的数值不能转换成正确数据类型时发生的异常、由属性设置器抛出的异常,以及由值转换器抛出的异常。
那么,当验证失败时会发生什么情况呢?可以使用System.Windows.Controls.Validation类的附加属性记录下验证错误。对于每个失败的验证规则,WPF采取以下三个步骤:
●       在绑定的元素上(在此是TextBox控件),将Validation.HasError附加属性设置为true。
●       创建包含错误细节的ValidationError对象(作为ValidationRule.Validate( )方法的返回值),并将该对象添加到关联的Validation.Errors集合中。
●       如果Binding.NotifyOnValidationError属性被设置为true,WPF就在元素上引发一个Validation.Error附加事件。
当发生错误时,绑定控件的可视化外观也会发生变化。当控件的Validation.HasError属性被设置为true时,WPF自动将控件使用的模板切换为由Validation.ErrorTemplate附加属性定义的模板。在文本框中,新的模板将文本框的轮廓改变成一个细的红色边框。
在大多数情况下,会希望以某种方式扩大错误指示,并提供与引发错误的问题相关的特定信息。可以使用代码处理Error事件,或者提供一个自定义的控件模板,从而提供不同的可视化指示。但是在执行这些任务之前,分析WPF提供的其他捕获错误的方式是值得的—— 通过使用数据对象中的IDataErrorInfo接口并编写自定义的验证规则。
2. DataErrorValidationRule验证规则
许多完全使用面向对象开发方法的开发人员,可能不喜欢通过引发异常来指示用户输入错误。这可能是因为以下几个原因:异常不是由于用户输入错误造成的,而可能是由于多个属性值之间的交互造成的,并且有时为了进行进一步的处理,保存不正确的数值是值得的,而不是完全拒绝它们。
在Windows窗体编程领域,开发人员可以使用IDataErrorInfo接口(位于System.ComponentModel名称空间)来避免异常,但是仍然可以在数据类中放置验证代码。最初设计IDataErrorInfo接口是用于支持基于网格的显示控件,如DataGridView控件,但是它也可以作为报告错误的专门的解决方案。尽管WPF的第一个发布版本不支持IDataErrorInfo接口,但是WPF 3.5版本支持该接口。
IDataErrorInfo接口需要两个成员:一个是被命名为Error的字符串属性和一个字符串索引器。Error属性提供了描述整个对象的完整错误字符串(可能是类似“非法数据”的简单字符串)。字符串索引器接受一个属性名称,并返回错误信息的相关细节。例如,如果为字符串索引器传递“UnitCost”,就可以相应地接收到“The UnitCost cannot be negative”这一类的内容。在此,最理想的情况是可以正常设置属性,没有任何混乱,并且索引器允许用户界面检查非法数据。整个类的错误逻辑被集中到了一个地方。
下面是Product类的修订版,该版本实现了IDataErrorInfo接口。尽管可以使用IDataErrorInfo接口为各种验证问题提供验证消息,但是在此验证逻辑只检查属性ModelNumer的错误:
public class Product : INotifyPropertyChanged, IDataErrorInfo
{
...
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set {
modelNumber = value;
OnPropertyChanged(new PropertyChangedEventArgs("ModelNumber"));
}
}
// Error handling takes place here.
public string this[string propertyName]
{
get
{
if (propertyName == "ModelNumber")
{
bool valid = true;
foreach (char c in ModelNumber)
{
if (!Char.IsLetterOrDigit(c))
{
valid = false;
break;
}
}
if (!valid)
return "The ModelNumber can only contain letters and numbers.";
}
return null;
}
}
// WPF doesn't use this property.
public string Error
{
get { return null; }
}
}
为了告知WPF使用IDataErrorInfo接口,并且当修改了一个属性后,为了使用该接口检查错误,必须为Binding.ValidationRules集合添加DataErrorValidationRule验证规则,如下所示:









此外,还可以通过创建为某些类型的错误抛出异常、并使用IDataErrorInfo接口报告其他错误的数据对象,来组合这两种方法。只需要确保同时使用ExceptionValidationRule和DataErrorValidationRule验证规则即可。
提示:
.NET 3.5提供了一种快捷方式。这种方式并不为绑定添加ExceptionValidationRule验证规则,而是将Binding.ValidatesOnExceptions属性设置为true。并不为绑定添加DataErrorValidationRule验证规则,而是将Binding.ValidatesOnDataErrors属性设置为true。
16.5.2  自定义验证规则
应用自定义验证规则的方法和应用自定义转换器的方法很类似。该方法定义一个继承自ValidationRule类(位于System.Windows.Controls名称空间)的类,并且为了执行自定义的验证重写Validate( )方法。如果需要,可以添加接受其他细节的属性,可以使用这些属性影响验证(例如,一个检查文本的验证规则可以包含一个Boolean类型的CaseSensitive属性)。
下面是一个完整的验证规则,该规则将decimal数值限制在指定的最小值和最大值之间。因为这个验证规则用于货币数值,所以默认情况下,最小值是0,而最大值是decimal类型能够容纳的最大值。然而,为了得到最大的灵活性,可以通过属性来配置这些细节:
public class PositivePriceRule : ValidationRule
{
private decimal min = 0;
private decimal max = Decimal.MaxValue;
public decimal Min
{
get { return min; }
set { min = value; }
}
public decimal Max
{
get { return max; }
set { max = value; }
}
public override ValidationResult Validate(object value,
CultureInfo cultureInfo)
{
decimal price = 0;
try
{
if (((string)value).Length > 0)
price = Decimal.Parse((string)value, NumberStyles.Any,
culture);
}
catch
{
return new ValidationResult(false, "Illegal characters.");
}
if ((price < Min) || (price > Max))
{
return new ValidationResult(false,
"Not in the range " + Min + " to " + Max + ".");
}
else
{
return new ValidationResult(true, null);
}
}
}
注意,验证逻辑使用了Decimal.Parse ()方法的重载版本,该版本接受一个NumberStyles枚举值。这是因为验证总是在转换之前进行。如果为相同的字段同时应用验证器和转换器,需要确保当存在货币符号时能够成功地进行验证。验证逻辑的成功与失败通过返回的ValidationResult对象标识。IsValid属性指示验证是否成功,并且如果验证不成功,ErrorContent属性会提供一个描述问题的对象。在这个示例中,错误内容被设置为将会显示在用户界面中的字符串,这是最常用的方法。
一旦选择使用验证规则,就要准备通过将验证规则添加到Binding.ValidationRules集合中将它关联到一个元素。下面是使用PositivePriceRule验证规则的一个示例,该规则的Maximum属性值被设置为999.99:
Unit Cost:









通常,会为使用相同类型验证规则的每个对象定义单独的验证规则对象。因为可能希望独立地调整验证属性(例如,PositivePriceRule验证规则中的最小值和最大值)。如果确实希望为多个绑定使用相同的验证规则,可以将验证规则定义为资源,并在每个绑定中简单地使用StaticResource标记扩展指向该资源。
您可能已经猜到了,Bingding.ValidationRules集合可以包含任意数量的验证规则。当数值被提交到源时,WPF将按顺序检查每个验证规则(请记住,当文本框失去焦点时文本框的数值被提交到源,除非使用UpdateSourceTrigger属性指定为其他模式)。如果所有的验证都成功了, WPF就会调用转换器(如果存在的话)并为源应用数值。
注意:
如果在PositivePriceRule验证规则之后添加ExceptionValidationRule验证规则,会首先评估PositivePriceRule验证规则。PositivePriceRule验证规则将捕获由于超出范围造成的错误。然而,当输入的内容不能被转换为decimal类型的数值时(如一系列字母),ExceptionValidationRule验证规则会捕获类型转换错误。
当使用PositivePriceRule验证规则执行验证时,其行为和使用ExceptionValidationRule验证规则的行为相同—— 文本框使用红色绘制轮廓,设置HasError属性和Errors属性,并且引发Error事件。为了给用户提供一些更加有帮助的反馈,需要添加一些代码或自定义ErrorTemplate模板。在后续的几小节中,将会学习如何使用这两种方法。
提示:
自定义验证规则可以非常特殊,从而可以用于约束特定的属性,或者更通常的情况是它们可以在各种情况下重用。例如,可以很容易地创建一个自定义验证规则,借助于.NET提供的System.Text.RegularExpression.Regex类,使用特定的正则表达式检查字符串。根据使用的正则表达式,可以对各种基于模式的文本属性使用这个验证规则,如电子邮件地址、电话号码、IP地址以及邮政编码。
16.5.3  响应验证错误
在上一个示例中,有关用户接收到错误的唯一指示是在违反规则的文本框周围的红色轮廓。为了提供更多的信息,可以处理Error事件,当存储或清除错误时会引发该事件,但前提是首先必须确保已经将Binding.NotifyOnValidationError属性设置为true。

Error事件是一个使用冒泡策略的路由事件,所以可以通过在父包容器中关联事件处理程序为多个控件处理Error事件,如下所示:

下面的代码对这个事件进行响应,并显示一个具有错误信息的消息框(可能更好的一个选择是显示一个工具条提示或者在窗口的某个地方显示错误信息)。
private void validationError(object sender, ValidationErrorEventArgs e)
{
// Check that the error is being added (not cleared).
if (e.Action == ValidationErrorEventAction.Added)
{
MessageBox.Show(e.Error.ErrorContent.ToString());
}
}
ValidationErrorEventArgs.Error属性提供了一个ValidationError对象,该对象将几个有用的细节捆绑在一起,包括引起问题的异常(Exception)、违反的验证规则(ValidationRule)、关联的绑定对象(BindingInError),以及所有ValidationRule对象返回的自定义信息(ErrorContent)。
如果正在使用自定义的验证规则,几乎总是会选择在ValidationError.ErrorContent属性中放置错误信息。如果使用ExceptionValidationRule验证规则,ErrorContent属性将会返回相应异常的Message属性。然而,存在一个问题,如果是因为数值不能和数据类型相匹配而引起的异常,ErrorContent属性会如您所期望的那样工作,并报告发生了问题。但如果在数据对象的属性设置器中抛出了异常,这个异常会被包装到TargetInvocationException对象中,并且ErrorContent属性提供来自TargetInvocationException.Message属性的文本,该文本的内容是“Exception has been thrown by the target of an invocation.”,这一文本内容实际没有什么用处。
因此,如果使用由自定义的属性设置器引发的异常,就需要添加代码检查TargetInvocationException对象的InnerException属性。如果它不是null,就可以检索原始异常对象,并使用它的Message属性,而不是使用ValidationError.ErrorContent属性。
16.5.4  获取异常列表
在特殊的情况下,可能希望获取当前窗口(或窗口中特定包容器)中所有未处理异常的列表。这个任务相对比较简单—— 需要做的所有工作就是遍历元素树,检查每个元素的Validation. HasError属性。
下面的代码示例专门查找TextBox对象中的非法数据。这个示例使用递归代码遍历整个元素层次。同时,错误信息被聚集到一个单独的消息中,然后显示给用户。
private void cmdOK_Click(object sender, RoutedEventArgs e)
{
string message;
if (FormHasErrors(message))
{
// Errors still exist.
MessageBox.Show(message);
}
else
{
// There are no errors. You can continue on to complete the task
// (for example, apply the edit to the data source.).
}
}
private bool FormHasErrors(out string message)
{
StringBuilder sb = new StringBuilder();
GetErrors(sb, gridProductDetails);
message = sb.ToString();
return message != "";
}
private void GetErrors(StringBuilder sb, DependencyObject obj)
{
foreach (object child in LogicalTreeHelper.GetChildren(obj))
{
TextBox element = child as TextBox;
if (element == null) continue;
if (Validation.GetHasError(element))
{
sb.Append(element.Text + " has errors:\r\n");
foreach (ValidationError error in Validation.GetErrors(element))
{
sb.Append(" " + error.ErrorContent.ToString());
sb.Append("\r\n");
}
}
// Check the children of this object for errors.
GetErrors(sb, element);
}
}
}
在更复杂的实现中,FormHasErrors( )方法可能会创建一个包含错误信息对象的集合。然后cmdOK_Click( )事件处理程序会负责构造一个恰当的消息。
16.5.5  显示不同的错误指示符号
为了最大限度地使用WPF验证,您可能希望创建自己的错误模板,以适当的方式标识错误。乍一看,这好像是一种报告错误的低级方法—— 毕竟,标准的控件模板具有详细自定义控件构成的能力。然而,错误模板和普通的控件模板不同。
错误模板使用的是装饰层,装饰层是位于普通窗口之上的绘图层。使用装饰层,可以添加可视化装饰指示错误,而不用提供控件背后的控件模板,或者改变窗口的布局。文本框的标准错误模板通过在相应文本框的上面添加一个红色的Border元素(背后的文本框没有发生变化),指示发生了错误。可以使用错误模板添加其他细节,如图像、文本或者其他能够吸引注意力的图形细节。
下面的标记示例定义了一个错误模板,该模板使用绿色边框并在具有非法输入控件的下面添加了一个星号。该模板被包装进一个样式规则中,这样就可以自动将它应用到当前窗口的所有文本框:

AdornedElementPlaceholder元素是这种技术工作的粘合剂。它代表控件自身,位于元素层中。通过使用AdornedElementPlaceholder元素,能够在文本框的背后安排自己的内容。
因此,在这个示例中边框被直接放置在文本框之上,而不管文本框的尺寸是多少。在这个示例中,星号被放置在右边(如图16-13所示)。最好的是,新的错误模板内容叠加在已经存在的内容之上,从而不会在原始窗口的布局中触发任何改变(实际上,如果不小心在装饰层中包含了更多的内容,就会改变窗口的其他部分)。
图16-13  使用错误模板标示错误
提示:
如果希望错误模板叠加显示在元素之上(而不是位于它的周围),可以在Grid控件的同一个单元格中同时放置自定义的内容和AdornerElementPlaceholder元素。此外,也可以不使用AdornerElementPlaceholder元素,但这样就会丧失在元素之后精确定位自定义内容的能力。
错误模板也存在一个问题—— 它没有提供任何有关错误的附加信息。为了显示这些细节,需要使用数据绑定提取它们。一个好的方法是使用第一个错误的错误内容,并将它用作自定义错误指示器的工具提示文本。下面的模板实现了这一功能:


Foreground="Red" FontSize="14" FontWeight="Bold"
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
>*







绑定表达式的Path有些复杂,并且使用的是最近的检查。绑定表达式的源是AdornedElement Placeholder元素,该元素在控件模板中定义。
ToolTip="{Binding ElementName=adornerPlaceholder, ...
AdornedElementPlaceholder类通过AdornedElement属性提供了指向背后元素(在这个示例中是具有错误的TextBox对象)的引用:
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement ...
为了检索实际的错误,需要检查这个元素的Validation.Error属性。然而,需要使用圆括号包装Validation.Errors属性,以指示它是一个附加属性,而不是TextBox类的属性:
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors) ...
最后,需要使用索引器从集合中检索第一个ValidationError对象,然后提取该对象中Error对象的内容属性:
ToolTip="{Binding ElementName=adornerPlaceholder,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"
现在将鼠标移动到星号上,就可以看到错误消息。
此外,您可能还希望在Border元素或TextBox元素自身的工具提示中显示错误消息,从而当用户将鼠标移动到控件上的任何部分时会显示错误消息。我们可以实现这一功能而无需借助于自定义错误模板—— 只需要一个用于TextBox控件的触发器,当Validation.HasError属性变为true时应用该触发器,并且应用具有错误消息的工具提示。下面是一个例子:

图16-14显示了结果。
图16-14  将验证错误消息转移到工具提示中