到目前为止,很多人都知道使用 Windows®
Presentation Foundation (WPF) 可以轻松地设计强大的用户界面。但是您可能并不知道它还提供了强大的数据绑定功能。使用 WPF,可以通过利用 Microsoft®
.NET Framework 代码、XAML 或两者的组合进行数据操作。您可以绑定控件、公共属性、XML 或对象,从而使数据绑定比以前更快捷、灵活和简单。所以,让我们来看一下如何开始将控件绑定到您所选的数据源中。
数据绑定细节
要
使用 WPF 数据绑定功能,您必须始终要有目标和源。绑定的目标可以是从 DependencyProperty
派生而来的任何可访问属性或元素,例如 TextBox 控件的 Text 属性。绑定的源可以是任何公共属性,包括其他控件、公共语言运行库
(CLR) 对象、XAML 元素、ADO.NET Dataset、XML 片段等的属性。为了帮助您正确实现绑定,WPF
包含了两个特殊的提供程序:XmlDataProvider 和 ObjectDataProvider。
现在让我们看一下 WPF 数据绑定技术的工作原理,我将列举一些实用的示例来说明它们的用法。
创建简单的绑定
首先,我们来看一个简单的示例,该示例说明了如何将 TextBlock 的 Text 属性绑定到 ListBox 的选定项。图 1
中的代码显示的是声明了六个 ListBoxItem 的 ListBox。该代码示例中的第二个 TextBlock 具有名为 Text(使用
XML 子元素 <TextBlock.Text> 在 XAML 属性元素语法中指定)的属性,它将包含 TextBlock
的文本。Text 属性声明了通过 <Binding> 标记与 ListBox 选定项的绑定。Binding 标记的
ElementName 属性指示 TextBlock 的 Text 属性要与其绑定的控件的名称。Path 属性指示我们将绑定到的元素(在本例中是
ListBox)的属性。此代码产生的结果是,如果从 ListBox 选择了一种颜色,该颜色的名称则会在 TextBlock 中显示。

<StackPanel>
<TextBlock Width="248" Height="24" Text="Colors:"
TextWrapping="Wrap"/>
<ListBox x:Name="lbColor" Width="248" Height="56">
<ListBoxItem Content="Blue"/>
<ListBoxItem Content="Green"/>
<ListBoxItem Content="Yellow"/>
<ListBoxItem Content="Red"/>
<ListBoxItem Content="Purple"/>
<ListBoxItem Content="Orange"/>
</ListBox>
<TextBlock Width="248" Height="24" Text="You selected color:" />
<TextBlock Width="248" Height="24">
<TextBlock.Text>
<Binding ElementName="lbColor" Path="SelectedItem.Content"/>
</TextBlock.Text>
</TextBlock>
</StackPanel>
为了使用简单的语法来进行数据绑定,可以对图 1
中列出的代码稍加修改。例如,我们用下列代码段替代 TextBlock 的 <Binding> 标记:
<TextBlock Width="248" Height="24"
Text="{Binding ElementName=lbColor,
Path=SelectedItem.Content}" />
这种语法称为属性语法,它压缩了 TextBlock 的 Text 属性内部的数据绑定代码。基本上,Binding 标记会连同它的属性一起被归入大括号内。
绑定模式
我可以继续以上述示例为例,将 TextBlock 的背景色绑定到在 ListBox 中选择的颜色。以下代码可将 Background 属性添加到 TextBlock 中,并使用该属性的绑定语法将其绑定到 ListBox 中选定项的值:
<TextBlock Width="248" Height="24"
Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=OneWay}"
x:Name="tbSelectedColor"
Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=OneWay}"/>
如果用户在 ListBox 中选择了一种颜色,那么该颜色的名称就会出现在 TextBlock 中,并且 TextBlock 的背景色会变为选定的颜色(请参见图 2
)。

图 2 将一个源绑定到两个目标
请注意前一个示例中将 Mode 属性设为 OneWay 的语句。Mode 属性用于定义绑定模式,它将决定数据如何在源和目标之间流动。除 OneWay 之外,还有另外三种绑定模式:OneTime、OneWayToSource 和 TwoWay。
正
如前面的代码段中所示,使用 OneWay 绑定时,每当源发生变化,数据就会从源流向目标。尽管我在示例中显式指定了此绑定模式,但其实 OneWay
绑定是 TextBlock 的 Text 属性的默认绑定模式,无需对其指定。和 OneWay 绑定一样,OneTime
绑定也会将数据从源发送到目标;但是,仅当启动了应用程序或 DataContext 发生更改时才会如此操作,因此,它不会侦听源中的更改通知。与
OneWay 和 OneTime 绑定不同,OneWayToSource 绑定会将数据从目标发送到源。最后,TwoWay
绑定会将源数据发送到目标,但如果目标属性的值发生变化,则会将它们发回给源。
在
上述示例中,我使用了 OneWay 绑定,因为我希望只要 ListBox 选择发生变化,就将源(选定的 ListBoxItem)发送到
TextBlock。我不希望 TextBlock 的更改再回到 ListBox。当然,用户无法编辑 TextBlock。如果我想使用
TwoWay 绑定,可以将 TextBox 添加到此代码中,将其文本和背景色绑定到 ListBox,并将 Mode 属性设为
TwoWay。用户在 ListBox 中选择一种颜色后,该颜色就会显示在 TextBox 中,并且其背景色会相应变化。如果该用户在
TextBox 中键入了一种颜色(例如蓝绿色),ListBox 中的颜色名称就会更新(从目标到源),反过来,因为 ListBox
已经更新,所以此新值就会被发送到绑定到 ListBox 的 SelectedItem 属性的所有元素。这意味着 TextBlock
也会更新其颜色,并且将其文本值设置为该新的颜色(请参见图 3
)。

图 3 运行中的 TwoWay 绑定
下面是我刚才用来将 TextBlock (OneWay) 和 TextBox (TwoWay) 绑定到 ListBox 的代码:
<TextBlock Width="248" Height="24"
Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=OneWay}" x:Name="tbSelectedColor"
Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=OneWay}"/>
<TextBox Width="248" Height="24"
Text="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=TwoWay}" x:Name="txtSelectedColor"
Background="{Binding ElementName=lbColor, Path=SelectedItem.Content,
Mode=OneWay}"/>
如果我将 TwoWay 模式改回到 OneWay,用户则可以编辑 TextBox 中的颜色,且不会导致更改过的值被发回给 ListBox。
选
择合适的绑定模式非常重要。当我想向用户显示只读数据时,我通常会采用 OneWay
模式。当我希望用户可以更改控件中的数据,并且让该变化能在数据源(DataSet、对象、XML 或其他绑定控件)中体现出来时,我会使用
TwoWay 绑定。如果想让用户在数据源不将其数据绑定到目标的情况下更改数据源,我发现 OneWayToSource
是个不错的选择。我曾经接到一个任务,要求在只读控件中显示与加载屏幕时一样的数据状态。在这个任务中,我使用了 OneTime 绑定。通过使用
OneTime
绑定,一系列只读控件均被绑定到了数据,并且当用户与表单交互且数据源的值发生更改时,绑定控件仍保持不变。这为用户提供了一种比较所发生更改的方法。此
外,当源没有实现 INotifyPropertyChanged 时,OneTime 绑定也是一个不错的选择。
绑定的时间
在
上述示例中,TextBox 允许 TwoWay 绑定到在 ListBox 中选定的 ListBoxItem。这会使数据在 TextBox
失去焦点时从 TextBox 流回 ListBox。为了改变导致将数据发送回源的这种情况,可以为 UpdateSourceTrigger
指定值,它是用于定义何时更新源的绑定属性。可以为 UpdateSourceTrigger 设置三个值:Explicit、LostFocus 和
PropertyChanged。
如
果将 UpdateSourceTrigger 设置为 Explicit,则不会更新源,除非从代码调用
BindingExpression.UpdateSource 方法。LostFocus 设置(TextBox
控件的默认值)指示源在目标控件失去焦点时才会更新。PropertyChanged
值指示目标会在目标控件的绑定属性每次发生更改时更新源。如果您想指示绑定的时间,该设置非常有用。
绑定到 XML
绑定到数据源(例如 XML)和对象同样也非常方便。图 4
显示了 XmlDataProvider 的示例,其中包含将用作数据源的颜色的嵌入式列表。XmlDataProvider 可用来绑定到 XML 文档或片断,该文档或片段既可以嵌入在 XmlDataProvider 标记中,也可以位于外部位置引用的文件中。

<StackPanel>
<StackPanel.Resources>
<XmlDataProvider x:Key="MoreColors" XPath="/colors">
<x:XData>
<colors >
<color name="pink"/>
<color name="white"/>
<color name="black"/>
<color name="cyan"/>
<color name="gray"/>
<color name="magenta"/>
</colors>
</x:XData>
</XmlDataProvider>
嵌入式 XML 内容必须置于 XmlDataProvider 内部的 <x:XData> 标记中,如图 4
所示。必须为 XmlDataProvider 提供 x:Key 值,以便数据绑定目标可对其进行引用。请注意,XPath
属性设置为“/colors”。此属性定义了将用作数据源的 XML 内容的级别。当绑定到可能包含在文件或数据库中的较大 XML
结构,且想要绑定的数据不是根元素时,这一属性会变得非常有用。
XmlDataProvider 是可放置到特定上下文资源内部的一种资源。如图 4
所示,已将 XmlDataProvider 定义为 StackPanel 上下文中的资源。这意味着 XmlDataProvider 将可用于该
StackPanel
内部的所有内容。设置资源的上下文有助于限制数据源向合适的区域公开。这使您可以在页面内分别为控件和支持资源创建定义明确的独立区域,从而提高可读性。
绑
定到资源的语法与绑定到元素的语法略有不同。绑定到控件时,可以设置绑定的 ElementName 和 Path 属性。但是绑定到资源时,需要设置
Source 属性,由于我们是绑定到 XmlDataProvider,所以还要设置绑定的 XPath 属性。例如,下列代码可将 ListBox
的项绑定到 MoreColors 资源。将 Source 属性设置为资源,并将其指定为名为 MoreColors 的
StaticResource。XPath 属性指示项会绑定到 XML 数据源中 <color> 元素的名称属性:
<ListBox x:Name="lbColor" Width="248" Height="56"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Source={StaticResource MoreColors},
XPath=color/@name}">
</ListBox>
我
在本例中指定了 StaticResource,因为 XML
不会发生更改。如果数据源发生了更改,则不会将这些更改发送到目标。DynamicResource
设置则表示相反的情况,即会将数据源的更改发送到目标。当引用系统主题、全球化语言或字体时,该设置非常有用。DynamicResource
将允许这些类型的设置被传送到与其动态绑定的所有 UI 元素中。
XmlDataProvider
也可以指向 XML 内容的外部源。例如,我有一个名为 colors.xml 的文件,其中包含我希望 ListBox
绑定到的颜色列表。我只需要将第二个 XmlDataProvider 资源添加到 StackPanel,并将其引向 XML 文件即可。请注意,我将
Source 属性设置为了 XML 文件的名称,并将 x:Key 设置为 Colors:
<XmlDataProvider x:Key="Colors" Source="Colors.xml" XPath="/colors"/>
两个 XmlDataProvider 在同一个 StackPanel 中都作为资源而存在。我可以使 ListBox 将其本身绑定到这个新的资源,方法是更改 StaticResource 设置的名称:
<ListBox x:Name="lbColor" Width="248" Height="56"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Source={StaticResource Colors},
XPath=color/@name}">
</ListBox>
对象绑定和 DataTemplates
虽
然 XmlDataProvider 对 XML 非常有用,但是当您想绑定到对象或对象列表时,可以创建 ObjectDataProvider
作为资源。ObjectDataProvider 的 ObjectType 指定将提供数据绑定源的对象,而 MethodName
则指示为获得数据而需调用的方法。例如,假设我有一个名为 PersonService 的类,该类使用一种名为 GetPersonList
的方法来返回列表 <Person>,那么 ObjectDataProvider 可能会如下所示:
<StackPanel.Resources>
<ObjectDataProvider x:Key="persons"
ObjectType="{x:Type svc:PersonService}"
MethodName="GetPersonList"></ObjectDataProvider>
</StackPanel.Resources>
如果您想进行更全面的了解,本栏随附的代码中还包含了 PersonService 和 Person 类以及其他示例代码。
ObjectDataProvider
还可以使用许多其他属性。ConstructionParameters 属性允许您将参数传递给要调用的类的构造函数。此外,可以使用
MethodParameters 属性来指定参数,同时还可以使用 ObjectInstance 属性来指定现有的对象实例作为源。
如果希望异步检索数据,可以将 ObjectDataProvider 的 IsAsynchronous 属性设为 true。这样,用户将可以在等待数据填充绑定到 ObjectDataProvider 的源的目标控件时与屏幕进行交互。
在添加 ObjectDataProvider 时,必须限定数据源类的命名空间。在本例中,我必须将 xmlns 属性添加到 <Window> 标记中,以便 svc 快捷方式符合要求,并指示正确的命名空间:
xmlns:svc="clr-namespace:DataBindingWPF"
既
然数据源已通过 ObjectDataProvider 定义,接着我想将 ListBox 控件中的项绑定到此数据。我想在每个
ListBoxItem 中显示两行文本。第一行将以粗体显示 Person 实例的 FullName 属性,第二行将显示该实例的 Title 和
City。在 XAML 中,通过使用 DataTemplate,这是很容易实现的,DataTemplate 允许您定义可重用的数据可视化策略。
图
5 显示了完整的 XAML,其中将 DataTemplate 定义为在我指定的布局中显示 Person 信息。我设置了
DataTemplate 的 DataType 属性,以指示 DataTemplate 将会引用 Person 类类型。我没有在
DataTemplate 中指定真正的绑定,因为我会在 ListBox 控件中指定。通过省略绑定源,可对作用域内的当前 DataContext
执行绑定。
在图 5
中,我将 ListBox 的 ItemsSource 属性设置为绑定到人员资源,以便我可以将数据绑定到
ListBox,而不用对它进行格式化。通过将 ItemTemplate 属性设置为 PersonLayout 资源(即 DataTemplate
的键名),可以正确显示数据。最终结果的屏幕如图 6
所示。

<Window x:Class="DataBindingWPF.ObjectBinding"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:svc="clr-namespace:DataBindingWPF"
Title="DataBindingWPF" Height="300" Width="300">
<StackPanel>
<StackPanel.Resources>
<ObjectDataProvider x:Key="persons"
ObjectType="{x:Type svc:PersonService}"
MethodName="GetPersonList" ></ObjectDataProvider>
<DataTemplate x:Key="personLayout" DataType="Person">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=FullName}"
FontWeight="Bold" Foreground="Blue">
</TextBlock>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Title}"></TextBlock>
<TextBlock Text=", "></TextBlock>
<TextBlock Text="{Binding Path=City}"></TextBlock>
</StackPanel>
</StackPanel>
</DataTemplate>
</StackPanel.Resources>
<TextBlock></TextBlock>
<ListBox x:Name="lbPersons"
ItemsSource="{Binding Source={StaticResource persons}}"
ItemTemplate="{DynamicResource personLayout}"
IsSynchronizedWithCurrentItem="True"/>
</StackPanel>
</Window>

图 6 使用 DataTemplate
对数据进行排序
如
果想以特定的方式对数据进行排序,可以绑定到 CollectionViewSource,而不是直接绑定到
ObjectDataProvider。CollectionViewSource 则会成为数据源,并充当截取 ObjectDataProvider
中的数据的媒介,并提供排序、分组和筛选功能,然后将它传送到目标。
接着显示的 CollectionViewSource 将其 Source 属性设置为 ObjectDataProvider(人员)的资源名称。然后我通过指示排序依据的属性及其方向定义了数据的排序顺序:
<CollectionViewSource x:Key="personView"
Source="{Binding Source={StaticResource persons}}">
<CollectionViewSource.SortDescriptions>
<ComponentModel:SortDescription
PropertyName="City"
Direction="Ascending" />
<ComponentModel:SortDescription
PropertyName="FullName"
Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
DataContext
可用来将容器控件内部的所有控件都绑定到数据源中。当您拥有多个控件,而且这些控件全部使用同一个绑定源时,这非常有用。如果为每个控件都指定了绑定源,
那么代码可能会重复。相反,可以将容器控件的 DataContext 设置为绑定源,然后只需从所包含的控件中省略 Source
属性即可。例如,下面是一系列显式绑定到同一个绑定源的 TextBlock:
<StackPanel>
<TextBlock Text="{Binding Source={StaticResource personView},
Path=FullName}"></TextBlock>
<TextBlock Text="{Binding Source={StaticResource personView},
Path=Title}"></TextBlock>
<TextBlock Text="{Binding Source={StaticResource personView},
Path=City}"></TextBlock>
</StackPanel>
以下是绑定到 DataContext 的三个相同的 TextBox,在此处,DataContext 反过来引用了该控件的 StackPanel 容器:
<StackPanel DataContext="{Binding Source={StaticResource personView}}" >
<TextBlock Text="{Binding Path=FullName}"></TextBlock>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
<TextBlock Text="{Binding Path=City}"></TextBlock>
</StackPanel>
如果该容器没有定义 DataContext,那么它会继续查找下一个外部嵌套容器,直到它找到当前的 DataContext 为止。