Windows Presentation Foundation 数据绑定

第一部分

本页内容

Windows Presentation Foundation 数据绑定 简介
Windows Presentation Foundation 数据绑定 简化的 XAML 绑定
Windows Presentation Foundation 数据绑定 我们所处的位置
Windows Presentation Foundation 数据绑定 参考资料

简介

Windows Presentation Foundation(以前称作 Avalon)为胖客户端开发用户界面引入了一个意义深远的新方法。WPF 第一次将用户界面设计与代码设计相分离。这种分离意味着,通常标记在一个文件中而代码则在另一个文件中,这与 ASP.NET 很类似。然而,这种分离仅在编译时存在。标记文件用于生成形成代码文件的代码,进而生成应用程序。

为了便于设计,Microsoft 开发了一种丰富的标记语言,称作 XAML。XAML 是一种基于 XML 的标记语言,它支持一个用于开发特定应用程序的新模型,这些应用程序具有对许多不同的用户界面概念的本机支持,如 2D 和 3D 绘图、动画、控件包容、控件和文档流,以及一个丰富的数据绑定模型。本文将概述 WPF 数据绑定,并假定您对 WPF 有一定的了解。如果您还不了解 WPF,请参阅 Tim Sneath 的 Architectural Overview of the Windows Presentation Foundation Beta 1 Release 一文进行概览。

为什么使用数据绑定?

如果您要开始使用 WPF,可能想知道:不用学习数据绑定,只编写代码来执行项目中的大部分数据操作是否更容易。虽然这可能是一个有效的方法,但我猜想您将逐渐使用基于 XAML 的数据绑定,甚至可能会爱上它。下面我们来看一个小示例。

图 1 显示一个简单 WPF 项目的用户界面。它是 RSS 提要的编辑器,允许用户查看和编辑提要。

Windows Presentation Foundation 数据绑定

图 1. 我们的 RSS 编辑器

该编辑器的布局相当简单,如以下 XAML 代码所示。

<Window x:Class="ExampleCS.Window1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Title="ExampleCS" 
    Loaded="Window1_Loaded" > 
  <StackPanel> 
    <TextBlock HorizontalAlignment="Center" FontWeight="Bold"> 
      BlogEditor 
    </TextBlock> 
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> 
       <ListBox Name="entryListBox" 
          Height="300" 
          SelectionChanged="entryListBox_Changed"/> 
       <Grid Width="500" Margin="5">
         <Grid.ColumnDefinitions>
           <ColumnDefinition Width="50" /> 
           <ColumnDefinition Width="*" />
       </Grid.ColumnDefinitions> 
       <Grid.RowDefinitions> 
         <RowDefinition Height="25" /> 
         <RowDefinition Height="25" /> 
         <RowDefinition Height="25" /> 
         <RowDefinition Height="*" /> 
         <RowDefinition Height="25" /> 
       </Grid.RowDefinitions> 
       <TextBlock Grid.Row="0" Grid.Column="0">Title:</TextBlock> 
       <TextBox Grid.Row="0" Grid.Column="1" Name="titleText" /> 
       <TextBlock Grid.Row="1" Grid.Column="0">Url:</TextBlock> 
       <TextBox Grid.Row="1" Grid.Column="1" Name="urlText" /> 
       <TextBlock Grid.Row="2" Grid.Column="0">Date:</TextBlock> 
       <TextBox Grid.Row="2" Grid.Column="1" Name="dateText" /> 
       <TextBlock Grid.Row="3" Grid.Column="0">Body:</TextBlock> 
       <TextBox Grid.Row="3" Grid.Column="1" 
          Name="bodyText" 
          TextWrapping="Wrap" /> 
       <Button Grid.Row="4" 
             Grid.ColumnSpan="2" 
             Grid.Column="1"
           Click="updateButton_Click"> 
         Update
       </Button> 
     </Grid> 
   </StackPanel> 
 </StackPanel> 
</Window> 

请注意加粗显示的事件处理程序。那里是大部分代码将发生的地方,加载 RSS 提要并填充 WPF 控件。

C#:

XMLDocument blog = new XMLDocument();
const string BLOGURL = @"z:\adoguy.RSS";

public Window1()
{
  InitializeComponent();
  blog.Load(BLOGURL);
}

void Window1_Loaded(object sender, RoutedEventArgs e) 
{
  FillListBox();
}

void FillListBox()
{
  entryListBox.Items.Clear();

  XMLNodeList nodes = blog.SelectNodes("//item");
  foreach (XMLNode node in nodes)
  {
    ListBoxItem item = new ListBoxItem();
    item.Tag = node;
    item.Content = node["title"].InnerText;
    entryListBox.Items.Add(item);
  }
}

Visual Basic .NET:

  Dim blog As New XMLDocument
  Const BLOGURL As String = "z:\adoguy.RSS"

  Public Sub New()
    InitializeComponent()
    blog.Load(BLOGURL)
  End Sub

  Sub Window1_Loaded(ByVal sender As Object, _
                     ByVal e As RoutedEventArgs) 
    FillListBox()

  End Sub

  Sub FillListBox()

    entryListBox.Items.Clear()

    Dim nodes As XMLNodeList = blog.SelectNodes("//item")

    For Each node As XMLNode In nodes

      Dim item As New ListBoxItem
      item.Tag = node
      item.Content = node("title").InnerText
      entryListBox.Items.Add(item)

    Next

  End Sub

在这段代码中,您可以看到:我们用 RSS 提要加载一个 XML 文档,用所有项的标题填充 ListBox

接下来,我们需要处理在 ListBox 中进行选择时将发生的事情。

C#:

void entryListBox_Changed(object sender, RoutedEventArgs e)
{
  if (entryListBox.SelectedIndex != -1)
  {
    XMLNode node = ((ListBoxItem)entryListBox.SelectedItem).Tag as XMLNode;
    if (node != null)
    {
      titleText.Text = node["title"].InnerText;
      URLText.Text = node["guid"].InnerText;
      dateText.Text = node["pubDate"].InnerText;
      bodyText.Text = node["description"].InnerText;
    }
  }
}

Visual Basic .NET:

  Sub entryListBox_Changed(ByVal sender As Object, _
                           ByVal e As SelectionChangedEventArgs) 

    If entryListBox.SelectedIndex <> -1 Then

      Dim node As XMLNode = CType(entryListBox.SelectedItem, ListBoxItem).Tag
      If Not node Is Nothing Then

        titleText.Text = node("title").InnerText
        URLText.Text = node("guid").InnerText
        dateText.Text = node("pubDate").InnerText
        bodyText.Text = node("description").InnerText

      End If

    End If

  End Sub

ListBox 更改时,我们用所选的 RSS 提要项填充 TextBoxes。可以通过获取提要项的内部文本来执行该操作。请注意,我们还需要用 ListBoxItem, 保留节点的一份副本,进行一些转换以到达每个事件处理程序。

最后,我们需要处理单击 Update 按钮时发生的事情。

C#:

void updateButton_Click(object sender, RoutedEventArgs e)
{
  if (entryListBox.SelectedIndex != -1)
  {
    XMLNode node = ((ListBoxItem)entryListBox.SelectedItem).Tag as XMLNode;
    if (node != null)
    {
      node["title"].InnerText = titleText.Text;
      node["guid"].InnerText = URLText.Text;
      node["pubDate"].InnerText = dateText.Text;
      node["description"].InnerText = bodyText.Text;

      blog.Save(BLOGURL);

      FillListBox();
    }
  }
}

Visual Basic .NET:

  Sub updateButton_Click(ByVal sender As Object, _
                         ByVal e As RoutedEventArgs)

    If entryListBox.SelectedIndex <> -1 Then

      Dim node As XMLNode = CType(entryListBox.SelectedItem, ListBoxItem).Tag

      If Not node Is Nothing Then

        node("title").InnerText = titleText.Text
        node("guid").InnerText = URLText.Text
        node("pubDate").InnerText = dateText.Text
        node("description").InnerText = bodyText.Text

        blog.Save(BLOGURL)

        FillListBox()

      End If

    End If

  End Sub

在此处,假如更改了标题,我们用新信息更新节点,保存 XML 文档,并导致重新填充 ListBox。尽管这只是一个简单的小应用程序,但需要的代码还是有点儿多。使用 XAML 数据绑定执行该操作是否会简单些?是的,会更简单。以下是同一个项目的 XAML,使用 XAML 数据绑定。

  <StackPanel.Resources>
    <b><XMLDataProvider x:Key="RSSFeed" Source="z:\adoguy.RSS" /></b>
  </StackPanel.Resources>
  <TextBlock HorizontalAlignment="Center" 
      FontWeight="Bold">
    Blog Editor
  </TextBlock>
  <StackPanel Orientation="Horizontal" 
    HorizontalAlignment="Center">
    <ListBox Name="entryListBox" 
        Height="300" 
        <b>ItemsSource="{Binding Source={StaticResource RSSFeed}, XPath=//item}"</b>
    >
      <ListBox.ItemTemplate>
        <DataTemplate>
          <TextBlock <b>Text="{Binding XPath=title}"</b> />
        </DataTemplate>
      </ListBox.ItemTemplate>
    </ListBox>
    <Grid Width="500" 
        Margin="5" 
        <b>DataContext="{Binding ElementName=entryListBox, Path=SelectedItem}"</b> >
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="50" />
        <ColumnDefinition Width="*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="25" />
        <RowDefinition Height="25" />
        <RowDefinition Height="25" />
        <RowDefinition Height="*" />
        <RowDefinition Height="25" />
      </Grid.RowDefinitions>
      <TextBlock Grid.Row="0" Grid.Column="0">Title:</TextBlock>
      <TextBox Grid.Row="0" Grid.Column="1" 
          Name="titleText" 
          <b>Text="{Binding XPath=title}"</b> />
      <TextBlock Grid.Row="1" Grid.Column="0">URL:</TextBlock>
      <TextBox Grid.Row="1" Grid.Column="1" 
          Name="URLText" 
<b>          Text="{Binding XPath=guid}"</b> />
      <TextBlock Grid.Row="2" Grid.Column="0">Date:</TextBlock>
      <TextBox Grid.Row="2" Grid.Column="1" 
          Name="dateText" 
          <b>Text="{Binding XPath=pubDate}"</b> />
      <TextBlock Grid.Row="3" Grid.Column="0">Body:</TextBlock>
      <TextBox Grid.Row="3" Grid.Column="1" 
          Name="bodyText" 
          TextWrapping="Wrap" 
          <b>Text="{Binding XPath=description}"</b> />
      <Button Grid.Row="4" Grid.ColumnSpan="2" Grid.Column="1" >
          Update
      </Button>
    </Grid>
  </StackPanel>
</StackPanel>

这段 XAML 代码运行时没有任何隐藏的代码。图 2 显示只使用 XAML 的应用程序(使用 XAMLPad)。

Windows Presentation Foundation 数据绑定

图 2. XAMLPad 中的 RSS 编辑器

您可能想知道"它是如何运行的"?它之所以运行,是因为我们使用 XAML 绑定来为我们做大量的工作。虽然以后会详细介绍 XAML 中涉及的每项技术,但我们还是先浏览一下数据绑定的各部分内容,从而解释它在实际工作中的情况。

首先,创建一个 XMLDataProvider 对象来加载和管理编辑器的 XML 文档。

<XmlDataProvider x:Key="RssFeed" Source="z:\adoguy.RSS" />

它告诉 XAML 从 Z: 驱动器加载一个 XML 文档,以填充窗体。

接下来,绑定 ListBox

<ListBox Name="entryListBox" Height="300" ItemsSource="{Binding Source={StaticResource RssFeed}, XPath=//item}" > 

这段代码告诉列表框查找名为 RSSFeed 的 XAML 中的资源,然后将其用作数据源。此外,它还告诉列表框从 XPath 表达式获得要绑定到的项的列表(在本例中是文档中的所有项元素)。

接下来,通过指定数据模板来指定要在列表框的各项中显示的内容。

<DataTemplate> <TextBlock Text="{Binding XPath=title}" /> </DataTemplate> 

这段代码告诉 ListBox 为每项创建一个 TextBlock 对象,并使用 XPath 确定要在 TextBlock 中显示的内容。

接下来,绑定包含所有详细信息的网格。

<Grid Width="500" Margin="5" DataContext="{Binding ElementName=entryListBox, Path=SelectedItem}" > 

在此处,我们告诉 XAML 将容器(在本例中是 Grid)绑定到 ListBox 的选定项。我们使用 ElementName 而不是 Source,因为我们希望绑定到 XAML 文档中的某个控件,而不是一些外部数据。通过设置 DataContext,允许将 Grid 中的所有控件设置为同一个对象(在本例中是 SelectedItem)。

接下来,我们将每个 TextBox 绑定到所需的 XML 文档部分。

<TextBlock Grid.Row="0" Grid.Column="0">Title:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="1" Name="titleText" Text="{Binding XPath=title}" />
<TextBlock Grid.Row="1" Grid.Column="0">Url:</TextBlock> 
<TextBox Grid.Row="1" Grid.Column="1" Name="urlText" Text="{Binding XPath=guid}" /> 
<TextBlock Grid.Row="2" Grid.Column="0">Date:</TextBlock> 
<TextBox Grid.Row="2" Grid.Column="1" Name="dateText" Text="{Binding XPath=pubDate}" />
<TextBlock Grid.Row="3" Grid.Column="0">Body:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Name="bodyText" TextWrapping="Wrap" Text="{Binding XPath=description}" /> 

由于我们设置了 DataContext,因此可以仅指定 XPath 表达式,以获得我们需要的 RSS 提要的部分。

一下尝试接受这些内容太多了。停下来喘口气。我并不指望你们一下子都能接受。在下面部分中,我将前面示例的许多观点分成各个更易于管理的部分。

简化的 XAML 绑定

要从一个有关 Binding 对象如何工作的简单示例开始,我们先从下面这个非常简单的 XAML 文档入手。

  <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" > 
      <Canvas> 
        <TextBox Text="This is a TextBox" />
        <TextBlock Canvas.Top="25" >Test</TextBlock> 
      </Canvas> 
  </Window> 

这段代码创建了一个简单的画布,包含两个控件,如图 3 所示。

Windows Presentation Foundation 数据绑定

图 3. 简单的 XAML 画布

我们可能希望绑定 TextBlock,以便按照键入的方式显示 TextBox 的文本。为此,我们需要一个 Binding 对象将两个对象绑定在一起。首先要向 TextBox 添加一个名称,这样我们才能通过元素名引用元素。

<TextBox Name="theTextBox" /> 

接下来,我们需要向 TextBlockText 元素添加一个 Binding

<TextBlock Canvas.Top="25"> <TextBlock.Text> <Binding ElementName="theTextBox" Path="Text" /> </TextBlock.Text> </TextBlock> 

这段代码告诉 TextBlockText 要设置为 TextBox 控件中的用户类型。

将以上这几段代码组合在一起即可产生以下代码。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" > 
  <Canvas> 
      <TextBox Name="theTextBox" Text="Hello" /> 
      <TextBlock Canvas.Top="25"> 
        <TextBlock.Text> 
          <Binding ElementName="theTextBox" Path="Text" /> 
        </TextBlock.Text> 
      </TextBlock> 
   </Canvas> 
</Window> 

这段 XAML 代码创建了一个窗体,该窗体按照在 TextBox 中键入的内容更改 TextBlock,如图 4 所示。

Windows Presentation Foundation 数据绑定

图 4. 绑定控件

祝贺您!您已经拥有了自己的第一个绑定!但是这个 XML 语法有点冗长。应该有更好的编写方法。

使用绑定简化版本

在上一示例中我们了解到,如何将 Binding 元素添加到属性来创建数据绑定。在这个简单的示例中,以这种方式进行数据绑定似乎不太麻烦;然而,随着 XAML 文档的增大,这个冗长的语法可能会变得比较麻烦。为了缓解这个问题,XAML 支持绑定语法的一个简化版本。

例如,使用简化版本,上一示例如下所示。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" > 
      <Canvas> 
        <TextBox Name="theTextBox" Text="Hello" /> 
        <TextBlock Canvas.Top="25" 
           Text="{Binding ElementName=theTextBox, Path=Text}" />
      </Canvas> 
</Window> 

简化语法包含在大括号 ({}) 中,以单词 Binding 开始,包含绑定属性的名称/值对。简化语法的目的是使数据绑定需要更少的击键,并且更易于阅读。本文的剩余部分,我将使用简化语法。

绑定源

目前为止,我们已经将另一个控件用作所有绑定示例的源。然而,在大多数基于 XAML 的项目中,您将绑定到除其他控件外的源。大多数数据绑定的关键是 Source 属性。在前面的示例中,我们使用的是 ElementName 属性(它用于绑定到一个控件),而不是使用 Source 属性。对于大多数应用程序,您希望绑定到更重要的源,如 XML 或 .NET 对象。XAML 用其 provider 对象支持该操作。XAML 中内置有两种类型的数据提供程序:ObjectDataProviderXMLDataProviderObjectDataProvider 用于绑定到 .NET 对象以及从 .NET 对象绑定,并不奇怪的是,XMLDataProvider 用于绑定到 XML 片段和文档以及从 XML 片段和文档进行绑定。您可以在任何 XAML 容器的资源部分中指定一个数据提供程序。

使用 XMLDataProvider

例如:

<StackPanel 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
   <StackPanel.Resources> 
     <XmlDataProvider x:Key="FavoriteColors"> 
       <x:XData>
         <Colors xmlns=""> 
           <Color>Blue</Color> 
           <Color>Black</Color> 
           <Color>Green</Color> 
           <Color>Red</Color> 
         </Colors> 
       </x:XData> 
    </XmlDataProvider> 
   </StackPanel.Resources> 
   <TextBlock HorizontalAlignment="Center" FontWeight="Bold"> 
      XML Example 
   </TextBlock> 
   <ListBox Width="200" Height="300" 
               ItemsSource="{Binding Source={StaticResource FavoriteColors}, 
               XPath=/Colors/Color}"> 
   </ListBox> 
</StackPanel> 

StackPanel 的资源中,我们有一个 XMLDataProvider 对象。x:Key 表示 Binding 对象中用来引用它的名称。在该提供程序中,我们创建了 XML 内联,用作数据绑定的源。在 ListBoxBinding 中,我们将提供程序指定为绑定的 Source。如果某个数据源位于 XAML 文档中,您需要指定该对象是一个静态源,如此处所示。最后,我们使用 XPath 语句指定应该使用 XML 文档中的哪个集合来填充 ListBox。该代码生成图 5 所示的窗体。

Windows Presentation Foundation 数据绑定

图 5. XML 数据绑定

或者,我们可以指定:该提供程序应该使用路径或 URL 来查找用来创建相同窗体的 XML 的源,方法是指定 XMLDataProviderSource 属性。

<XmlDataProvider x:Key="FavoriteColors" Source="D:\Writing\msdn\avalondatabindingpt1\xaml\colors.xml" /> 

XMLDataProviderSource 属性也可以指向标准 URL,从而使您能够创建到 XML API(如 RSS)的快速访问。

<StackPanel 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > 
 <StackPanel.Resources> 
   <XmlDataProvider x:Key="MyRSS" 
    Source="http://adoguy.com/RSS" /> 
 </StackPanel.Resources> 
 <TextBlock HorizontalAlignment="Center" 
               FontWeight="Bold"> 
   XML Example 
 </TextBlock> 
 <ListBox Width="500" Height="300" 
             ItemsSource="{Binding Source={StaticResource MyRSS}, 
             XPath=//item/title}"> 
 </ListBox> 
</StackPanel> 

通过调出到一个 RSS 提要,可以创建一个页面,它在 ListBox 中快速列出我的网络日记中的所有主题,如图 6 所示。

Windows Presentation Foundation 数据绑定

图 6. 我的网络日记的主题列表

使用 ObjectDataProvider

有时需要到 .NET 对象的绑定。这就是引入 ObjectDataProvider 的地方。该数据提供程序使您能够为 .NET 数据类型创建绑定。

例如,我们可以在 .NET 中创建一个简单的字符串集合,如下所示。

public class MyStrings : List
{
  public MyStrings()
  {
    this.Add("Hello");
    this.Add("Goodbye");
    this.Add("Heya");
    this.Add("Cya");
  }
}

- 或者 -

Public Class MyStrings
  Inherits List(Of String)

  Public Sub New()
    Me.Add("Hello")
    Me.Add("Goodbye")
    Me.Add("Heya")
    Me.Add("Cya")
  End Sub

End Class

也可以在 XAML 文档中使用一条处理指令将 CLR 对象的整个命名空间添加到该文档所支持的类型中,方法是将该命名空间指定为一个 xmlns 声明。例如,我们可以将整个命名空间的类映射到 XAML 中,如下所示。

<Window 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   Title="Simple Source Binding" 
   xmlns:local="clr-namespace:XamlExamples" 
   x:Class="XamlExamples.SimpleSource" > 
  <!-- ... --> 
</Window> 

要从外部程序集导入类,可以指定一个 xmlns 声明,仅指定程序集名称,如下所示。

<Window 
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   Title="Simple Source Binding" 
   xmlns:sys="clr-namespace:System,assembly=mscorlib" 
   x:Class="XamlExamples.SimpleSource" > 
 <!-- ... --> 
</Window> 

一旦已经导入了类型,就可以使用 ObjectDataProvider 指定来自其中一种类型的数据源。

<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Title="Simple Source Binding" 
    xmlns:local="clr-namespace:XamlExamples" 
    x:Class="XamlExamples.SimpleSource" >
  <Window.Resources>
    <ObjectDataProvider x:Key="MyStringData" 
       ObjectType="{x:Type local:MyStrings}" /> 
  </Window.Resources> 
    <StackPanel>
     <TextBlock HorizontalAlignment="Center" 
                  FontWeight="Bold"> 
     Simple Source Example 
     </TextBlock> 
     <ListBox Name="theListBox" Width="200" Height="300"
      ItemsSource="{Binding Source={StaticResource MyStringData}}"/> 
  </StackPanel> 
</Window> 

也可以通过要使用的命名空间和类型指定一个 XAML 元素,来创建资源,如下所示。

<Window 
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
     Title="Simple Source Binding" 
     xmlns:local="clr-namespace:XamlExamples" 
     x:Class="XamlExamples.SimpleSource" > 
  <Window.Resources> 
    <local:MyStrings x:Key="MyStringData" /> 
  </Window.Resources> 
    <StackPanel>
     <TextBlock HorizontalAlignment="Center" 
             FontWeight="Bold"> 
     Simple Source Example 
     </TextBlock>
   <ListBox Name="theListBox" Width="200" Height="300" 
       ItemsSource="{Binding Source={StaticResource MyStringData}}"/> 
  </StackPanel> 
</Window> 

该语法的工作方式就像 ObjectDataSource 一样,只不过更易于使用。既然已经导入了命名空间,我们就可以添加一个引用我们的数据源类的资源了,方法是指定该类的名称(例如, MyStrings)。数据绑定与前面的示例相同,因为 XAML 代码并不关心它是何种数据源,只要是数据源就可以了。

绑定模式

在大多数情况下,您希望绑定是一个双向绑定Binding 对象支持几种模式以支持不同的用例,如表 1 所示。

表 1. 绑定模式
绑定模式 说明

TwoWay

双向移动绑定控件或绑定源的改动。(这是默认模式。)

OneWay

仅将改动从源移动到控件。当源发生改动时,绑定控件的数据也相应更改。

OneTime

数据仅在启动时绑定,第一次用数据填充控件后忽略对源的更改。

只需在标记中包括模式即可指定模式,如下所示。

{Binding ElementName=theTextBox, Path=Text, Mode=OneTime}

一种了解双向绑定工作方式的方法是创建一个具有两个文本框的Canvas,一个文本框绑定到另一个文本框。

<Window 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" > 
  <Canvas>
    <TextBox Name="theTextBox" Text="Hello" /> 
    <TextBox Canvas.Top="25" 
                Text="{Binding ElementName=theTextBox, Path=Text, Mode=TwoWay}" /> 
    </Canvas> 
</Window> 

如果将这段代码粘贴到 SDK 附带的 XAMLPad 工具中,您应该注意到,源文本框按照您键入的内容更新绑定的文本框,但是,从绑定控件到源控件的更新仅发生在绑定控件失去焦点时。如果将 Mode 更改为 OneWayOneTime,就会看到这些不同的模式是如何更改绑定的工作方式的。

控制绑定时间

除了模式,您还可以使用UpdateSourceTrigger 指定绑定何时推出更改。您可以指定绑定仅在指定的时间进行更改,方法是指定 UpdateSourceTrigger 类型。

{Binding ElementName=theTextBox, Path=Text, UpdateSourceTrigger=LostFocus}

UpdateSourceTrigger 属性指定何时用改动更新源。这仅对 Mode=TwoWay 绑定(默认方式)有效。表 2 显示 UpdateSourceTrigger 的有效值。

表 2. UpdateSourceTrigger 值
UpdateSourceTrigger 说明

Explicit

源仅通过显式调用 BindingExpression.UpdateSource 方法更新。

LostFocus

绑定控件失去焦点时更新源。

PropertyChanged

每次属性更改时都将改动更新到源。这是默认行为。

使用 DataContext

本文要介绍的最后一个概念是如何使用DataContextDataContext 专门用于指定某个容器中的所有控件都将绑定到一个公共对象。

例如,以下是一个示例:我们有一个 Canvas,它将显示 XML 文档中特定节点的值和文本。

<StackPanel 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > 
 <StackPanel.Resources> 
  <XmlDataProvider x:Key="FavoriteColors"> 
   <Colors xmlns=""> 
    <Color ID="1">Blue</Color> 
    <Color ID="2">Black</Color> 
    <Color ID="3">Green</Color> 
    <Color ID="4">Red</Color> 
   </Colors> 
  </XmlDataProvider> 
 </StackPanel.Resources> 
 <TextBlock HorizontalAlignment="Center" 
               FontWeight="Bold">
   XML DataContext Example 
 </TextBlock> 
 <Canvas DataContext="{Binding Source={StaticResource FavoriteColors}, 
            XPath='/Colors/Color[@ID=2]'}"> 
  <TextBlock Text="{Binding [email protected]}" /> 
  <TextBlock Canvas.Top="25" Text="{Binding XPath=.}" /> 
 </Canvas> 
</StackPanel> 

通过将DataContext 设置为 XML 文档(以及一个特定的 XPath 表达式),我们告诉 Canvas:其中所有不包含 Source 的控件都可以使用容器的 DataContext。这样,只需指定 XPath 表达式就能绑定 TextBlock。请注意,每个 TextBlock 中的 XPath 表达式都是相对的 XPath 表达式。这对于对象绑定也一样。

<Window 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
  Title="Simple Source Binding" 
  x:Class="XamlExamples.SimpleSource" > 
 <Window.Resources> 
  <ObjectDataProvider TypeName="XamlExamples.MyStrings, XamlExamples" x:Key="MyStringData" /> 
 </Window.Resources> 
  <StackPanel> 
   <TextBlock HorizontalAlignment="Center" 
                 FontWeight="Bold"> 
    Object DataContext Example 
   </TextBlock>
   <Canvas DataContext="{Binding Source={StaticResource MyStringData}}"> 
     <TextBlock Text="{Binding Path=Length}" /> 
     <TextBlock Canvas.Top="25" Text="{Binding Path=Item[0]}" /> 
   </Canvas> 
 </StackPanel> 
</Window> 

使用对象代替 XML 仅仅意味着您将使用 Path 表达式代替 XPath 表达式。

第二部分

本页内容

Windows Presentation Foundation 数据绑定 简介
Windows Presentation Foundation 数据绑定 绑定到数据库数据
Windows Presentation Foundation 数据绑定 我们所处的位置
Windows Presentation Foundation 数据绑定 参考资料

简介

社区中大多数有争议的 WPF 示例都与图形引擎的问题有关。对于大多数用户界面开发人员而言,他们的大部分工作是在企业开发领域开发日常数据项窗体。WPF 是否有解决其问题的方法?当然有……

绑定到数据库数据

在本系列的第一部分中,我们探究了原始绑定语法以及如何将简单对象绑定到 XAML 对象。虽然这是该难题的一个重要部分,但大多数情况下,实际的要求是绑定到数据库中存储的数据。在大多数情况下,它支持两种不同方案中的绑定:数据库数据(例如,DataSetDataTableDataRow)和自定义业务对象。

绑定到数据库数据

目前,数据库仍然是进行大多数开发工作的中心,特别是企业开发。为了举例说明,我们可以使用一个简单的 WPF 对话框示例,它将允许用户浏览数据库中的雇员。我们希望能够在浏览器中显示一小部分信息,包括雇员照片。还需要加载一个包含所需全部信息的表。通过新建一个包含数据库信息的 DataTable,我们可以实现该操作:

在 C# 中:

DataTable theTable = new DataTable();
string connString = 
  ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
string query = @"SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo 
                 FROM Employees";

// Fill the Set with the data
using (SqlConnection conn = new SqlConnection(connString))
{
  SqlDataAdapter da = new SqlDataAdapter(query, conn);
  da.Fill(theTable);
}

在 Visual Basic .NET 中:

Dim theTable As DataTable =  New DataTable() 
String connString = 
  ConfigurationManager.ConnectionStrings("Northwind").ConnectionString
String query = "SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
               "  FROM Employees"
 
' Fill the Set with the data
Using conn as New SqlConnection(connString))
  Dim da As SqlDataAdapter =  New SqlDataAdapter(query,conn) 
  da.Fill(theTable)
End Using

我们拥有数据之后,可以使用这些数据设置 DataContext 以允许在 XAML 中对其进行绑定:

在 C# 中:

// Set the Data Context
DataContext = theTable;

在 Visual Basic .NET 中:

' Set the Data Context
DataContext = theTable

既然我们要获得数据并将其输出到窗口,我们就可以在 XAML 中进行数据绑定。ComboBox 中的 Binding 仅指示绑定从父级的 DataContext(在本例中,它沿控件树向上,直至在 Window 中找到一个 DataContext)获得数据:

<ComboBox Name="theCombo" 
         IsSynchronizedWithCurrentItem="True" 
         ItemsSource="{Binding}" 
         ... />

IsSynchronizedWithCurrentItem 属性很重要,因为当选择发生变化时,就窗口而言,是该属性更改"当前项"。它告诉 WPF 引擎将使用该对象更改当前项。如果没有该属性,DataContext 中的当前项不会改变;因此,您的文本框将假定当前项仍然是列表中的第一项。

要在组合框中显示雇员姓名,我们在 ItemsTemplate 中创建绑定以显示 DataTable 中的 FirstNameLastName

<DataTemplate x:Key="EmployeeListTemplate"> 
  <StackPanel Orientation="Horizontal"> 
  <TextBlock Text="{Binding Path=FirstName}" /> 
  <TextBlock Text=" " /> 
  <TextBlock Text="{Binding Path=LastName}" /> 
  </StackPanel> 
</DataTemplate>     

接下来,我们添加文本框以显示我们的姓名、头衔和雇佣日期:

<TextBlock Canvas.Top="5">First Name:</TextBlock> 
<TextBox Canvas.Top="5" Text="{Binding Path=FirstName}" /> 
<TextBlock Canvas.Top="25">Last Name:</TextBlock> 
<TextBox Canvas.Top="25" Text="{Binding Path=LastName}" /> 
<TextBlock Canvas.Top="45">Title:</TextBlock> 
<TextBox Canvas.Top="45" Text="{Binding Path=Title}" /> 
<TextBlock Canvas.Top="65">Hire Date:</TextBlock> 
<TextBox Canvas.Top="65" Text="{Binding Path=HireDate}" />       

由于我们也需要照片,因此需要向 XAML 添加一个图像:

<Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"/>

图像唯一的问题在于,它不支持将照片数据自动绑定到图像。为了便于该操作,我们可以处理 ComboBoxSelectionChanged 事件以填充我们的 Image:

   <ComboBox Name="theCombo" 
                IsSynchronizedWithCurrentItem="True" 
                Width="200" 
                ItemsSource="{Binding}" 
                ItemTemplate="{StaticResource EmployeeListTemplate}" 
                SelectionChanged="theCombo_OnSelectionChanged" />

在代码中,我们需要从 DataTable 加载图像,然后创建一个 BitmapImage 对象来填写 Image 标记。请注意,这不是 GDI+ (System.Drawing) 中的 Bitmap,而是 WPF 中新增的 Bitmap 对象:

// Handler to show the image
void theCombo_OnSelectionChanged(object sender, RoutedEventArgs e)
{
  ShowPhoto();
}

// Shows the Photo for the currently selected item
void ShowPhoto()
{
  object selected = theCombo.SelectedItem;
  DataRow row = ((DataRowView)selected).Row;
  
  // Get the raw bytes of the image
  byte[] photoSource = (byte[])row["Photo"];

  // Create the bitmap object
  // NOTE: This is *not* a GDI+ Bitmap object
  BitmapImage bitmap = new BitmapImage();
  MemoryStream strm = new MemoryStream();

  // Well-known work-around to make Northwind images work
  int offset = 78;
  strm.Write(photoSource, offset, photoSource.Length - offset);

  // Read the image into the bitmap object
  bitmap.BeginInit();
  bitmap.StreamSource = strm;
  bitmap.EndInit();

  // Set the Image with the Bitmap
  theImage.Source = bitmap;
  
}

在 Visual Basic .NET 中:

' Handler to show the image
Sub theCombo_OnSelectionChanged(ByVal sender As Object, ByVal e As RoutedEventArgs)

  ShowPhoto();

End Sub

// Shows the Photo for the currently selected item
Sub ShowPhoto()

  Dim selected As Object =  theCombo.SelectedItem 
  Dim row As DataRow = (CType(selected, DataRowView)).Row 
 
  ' Get the raw bytes of the image
  Dim photoSource() As Byte = CType(row("Photo"), Byte())
 
  ' Create the bitmap object
  ' NOTE: This is *not* a GDI+ Bitmap object
  Dim bitmap As BitmapImage =  New BitmapImage() 
  Dim strm As MemoryStream =  New MemoryStream() 
 
  ' Well-known work-around to make Northwind images work
  Dim offset As Integer =  78 
  strm.Write(photoSource, offset, photoSource.Length - offset)
 
  ' Read the image into the bitmap object
  bitmap.BeginInit()
  bitmap.StreamSource = strm
  bitmap.EndInit()
 
  ' Set the Image with the Bitmap
  theImage.Source = bitmap
 
End Sub

我们从 ComboBox 中抽取 SelectedItem 并将其转换成 DataRow,这样我们就可以获得自己的数据。然后,我们从 Photo 列抽取字节数组。这是存储在 Northwind 数据库中的照片。我们可以使用内存中流将照片字节流入到 BitmapImage 对象中。唯一的改动是常用的替代方案,即跳过 Northwind 图像头的前 78 个字节,因为不再使用这些字节。一旦我们将流读入位图中,就可以将其作为源分配给 Image 对象。

我们希望确保数据绑定是双向的,因此需要生成一个显示当前信息的按钮,这样我们就可以知道它在我们的 DataRow 中:

在 C# 中:

void SaveButton_OnClick(object sender, RoutedEventArgs e)
{
  object selected = theCombo.SelectedItem;
  DataRow row = ((DataRowView)selected).Row;

  MessageBox.Show(string.Format("{0} {1} {2} - {3:d}", 
    row["Title"], row["FirstName"], row["LastName"],  row["HireDate"]));
}

在 Visual Basic .NET 中:

Sub SaveButton_OnClick(ByVal sender As Object, ByVal e As RoutedEventArgs)

  Dim selected As Object =  theCombo.SelectedItem 
  Dim row As DataRow = (CType(selected, DataRowView)).Row 
 
  MessageBox.Show(String.Format("{0} {1} {2} - {3:d}", _
    row("Title"), row("FirstName"), row("LastName"),  row("HireDate")))

End Sub

完整的 XAML 文件其结尾部分如下所示:

<Window x:Class="ExampleCS.EmployeeBrowser"
    xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
    xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
    Title="Employee Browser"
    Loaded="OnLoaded" 
    Width="300"
    Height="170" 
    WindowStartupLocation="CenterScreen"
    >
  <Window.Resources>
    <DataTemplate x:Key="EmployeeListTemplate">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="<b>{Binding Path=FirstName}</b>" />
        <TextBlock Text=" " />
        <TextBlock Text="<b>{Binding Path=LastName}</b>" />
      </StackPanel>
    </DataTemplate>
  </Window.Resources>
  <Window.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
      <LinearGradientBrush.GradientStops>
        <GradientStop Color="DarkGray" Offset="0" />
        <GradientStop Color="White" Offset=".75" />
        <GradientStop Color="DarkGray" Offset="1" />
      </LinearGradientBrush.GradientStops>
    </LinearGradientBrush>
  </Window.Background>
  <StackPanel Name="theStackPanel" 
              VerticalAlignment="Top">
    <ComboBox Name="theCombo" 
              IsSynchronizedWithCurrentItem="True" 
              Width="200" 
              ItemsSource="<b>{Binding}</b>" 
              ItemTemplate="{StaticResource EmployeeListTemplate}"
              SelectionChanged="<b>theCombo_OnSelectionChanged</b>" />
    <Canvas>
      <Canvas.Resources>
        <Style TargetType="{x:Type TextBox}">
          <Setter Property="Canvas.Left" Value="160" />
          <Setter Property="Padding" Value="0" />
          <Setter Property="Height" Value="18" />
          <Setter Property="Width" Value="120" />
        </Style>
        <Style TargetType="{x:Type TextBlock}">
          <Setter Property="Canvas.Left" Value="90" />
          <Setter Property="Padding" Value="0" />
          <Setter Property="Height" Value="18" />
          <Setter Property="FontWeight" Value="Bold" />
        </Style>
      </Canvas.Resources>
      <Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"/>
      <TextBlock Canvas.Top="5">First Name:</TextBlock>
      <TextBox Canvas.Top="5" Text="<b>{Binding Path=FirstName}</b>" />
      <TextBlock Canvas.Top="25">Last Name:</TextBlock>
      <TextBox Canvas.Top="25" Text="<b>{Binding Path=LastName}</b>" />
      <TextBlock Canvas.Top="45">Title:</TextBlock>
      <TextBox Canvas.Top="45" Text="<b>{Binding Path=Title}</b>" />
      <TextBlock Canvas.Top="65">Hire Date:</TextBlock>
      <TextBox Canvas.Top="65" Text="<b>{Binding Path=HireDate}</b>" />
      <Button Canvas.Top="85" Canvas.Left="90" Width="190" 
              Name="SaveButton" Click="SaveButton_OnClick">Save</Button>
    </Canvas>
  </StackPanel>
</Window>

现在,如果我们运行浏览器,将获得如图 1 所示的界面:

Windows Presentation Foundation 数据绑定

图 1. 雇员浏览器

这个简单的示例相当简单易懂,但如果我们在 DataSet 中使用相关的 DataTable,该怎么办呢?我们看看是否一样简单。

绑定相关的 DataTable

我们可以扩展雇员浏览器以包括业务员的定单。为此,我们需要获得定单信息。我们可以在每次切换用户时利用一个新查询来实现该操作,不过,我们还是将数据随 Employee 一起加载到 DataSet 中,并使用 DataRelation 使这两部分信息相关:

在 C# 中:

DataSet theSet = new DataSet();

string connString = ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
string employeeQuery = @"
  SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo 
  FROM Employees
";
string orderQuery = @"
  SELECT o.OrderID, EmployeeID, CompanyName, OrderDate, SUM((UnitPrice * Quantity)* (1-Discount)) as OrderTotal
  FROM Orders o
  JOIN [Order Details] od on o.OrderID = od.OrderID
   JOIN Customers c on c.CustomerID = o.CustomerID
  GROUP BY o.OrderID, o.EmployeeID, o.OrderDate, CompanyName";

// Fill the Set with the data
using (SqlConnection conn = new SqlConnection(connString))
{
  SqlDataAdapter da = new SqlDataAdapter(employeeQuery, conn);
  da.Fill(theSet, "Employees");
  da.SelectCommand.CommandText = orderQuery;
  da.Fill(theSet, "Orders");
}

// Create the relationship
DataTable empTable = theSet.Tables["Employees"];
DataTable ordTable = theSet.Tables["Orders"];
theSet.Relations.Add("Emp2Ord", 
                     empTable.Columns["EmployeeID"], 
                     ordTable.Columns["EmployeeID"], 
                     false);

// Set the Context of the Window to be the 
// DataTable we've created
DataContext = empTable;

在 Visual Basic .NET 中:

Dim theSet As DataSet =  New DataSet() 
 
Dim connString As String = _
    ConfigurationManager.ConnectionStrings("Northwind").ConnectionString 
String employeeQuery = _
  "SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
  "  FROM Employees"

String orderQuery = _
  "SELECT o.OrderID, EmployeeID, CompanyName, OrderDate, " + _
  "       SUM((UnitPrice * Quantity)* (1-Discount)) as OrderTotal " + 
  "FROM Orders o " +
  "JOIN (Order Details) od on o.OrderID = od.OrderID " +
  "JOIN Customers c on c.CustomerID = o.CustomerID " +
  "GROUP BY o.OrderID, o.EmployeeID, o.OrderDate, CompanyName"
 
' Fill the Set with the data
Using conn as New SqlConnection(connString)

  Dim da As SqlDataAdapter =  New SqlDataAdapter(employeeQuery,conn) 
  da.Fill(theSet, "Employees")
  da.SelectCommand.CommandText = orderQuery
  da.Fill(theSet, "Orders")

End Using
 
' Create the relationship
Dim empTable As DataTable =  theSet.Tables("Employees") 
Dim ordTable As DataTable =  theSet.Tables("Orders") 
theSet.Relations.Add("Emp2Ord", 
                     empTable.Columns("EmployeeID"), 
                     ordTable.Columns("EmployeeID"), 
                     False)

' Set the Context of the Window to be the 
' DataTable we've created
DataContext = empTable

这段代码将创建一个具有两个表的 DataSet:Employees 和 Orders。这两个表通过 Emp2Ord 关系与 EmployeeID 相关。我们仍然可以绑定到 Employee DataTable,这样 XAML 中的原始数据绑定即可以正常工作。与 Windows 窗体或 ASP.NET 数据绑定非常类似,我们可以绑定到关系的名称,从而使我们能够绑定到一组相关记录:

 <ListBox Name="OrderList" Width="280" Height="200" 
     ItemsSource="{Binding Emp2Ord}" 
     ItemTemplate="{StaticResource OrderListTemplate}" />

该列表框仍然使用与雇员浏览器的其余部分相同的 DataContext;它仅通过关系指定绑定。一旦将列表框绑定到关系,我们就可以像在雇员组合框中那样绑定到 ItemTemplate 中的各个字段:

 <DataTemplate x:Key="OrderListTemplate"> 
    <StackPanel Orientation="Horizontal"> 
      <TextBlock VerticalAlignment="Top" Width="100" 
                    Text="{Binding Path=CompanyName}" /> 
    <StackPanel> 
      <TextBlock Text="{Binding Path=OrderID}" /> 
      <TextBlock Text="{Binding Path=OrderDate}" /> 
      <TextBlock Text="{Binding Path=OrderTotal}" /> 
      </StackPanel> 
    </StackPanel>
 </DataTemplate>

通过这个额外的数据绑定,我们现在正在显示一个列表框,仅包括与所选用户有关的定单信息:

Windows Presentation Foundation 数据绑定

图 2. 改进的雇员浏览器

这使我们能够绑定到更复杂的数据,而不仅仅是简单的成块数据。在许多组织中,它们使用自定义的 .NET 类型(或业务对象)来保存其数据和业务逻辑。WPF 会像 DataSet 一样轻松地绑定到这些对象吗?

绑定到"业务对象"

在 .NET 的最初表现形式(包括 Windows 窗体和 ASP.NET)中,DataSet 及其相关的对象是一等公民。它们简单地绑定数据,正常地工作。如果选择构建对象模型或业务对象来保存数据,您只能手动将对象中的数据绑定到控件。在 .NET 2.0 中,对象升级为一等公民,从而可以简化到对象的绑定。在 WPF 中也是一样。就像将对象作为 WPF 中的 DataSet 绑定一样简单。

要用业务对象创建喜爱的雇员浏览器,我们先创建一个类来保存 Employee

在 C# 中:

public class Employee
{
  // Fields
  int _employeeID;
  string _firstName;
  string _lastName;
  string _title;
  DateTime _hireDate;
  BitmapImage _photo;

  // Constructor
  public Employee(IDataRecord record)
  {
    _employeeID = (int) record["EmployeeID"];
    _firstName = (string) record["FirstName"];
    _lastName = (string)record["LastName"];
    _title = (string)record["Title"];
    _hireDate = (DateTime)record["HireDate"];
    CreatePhoto((byte[])record["Photo"]);
  }

  // BitmapImage creation
  void CreatePhoto(byte[] photoSource)
  {
    // Create the bitmap object
    // NOTE: This is *not* a GDI+ Bitmap object
    _photo = new BitmapImage();
    MemoryStream strm = new MemoryStream();

    // Well-known hack to make Northwind images work
    int offset = 78;
    strm.Write(photoSource, offset, photoSource.Length - offset);

    // Read the image into the bitmap object
    _photo.BeginInit();
    _photo.StreamSource = strm;
    _photo.EndInit();

  }
}

在 Visual Basic .NET 中:

Public Class Employee

  ' Fields
  Dim _employeeID As Integer
  Dim _firstName As String
  Dim _lastName As String
  Dim _title As String
  Dim _hireDate As DateTime
  Dim _photo As BitmapImage
 
  ' Constructor
  Public  Sub New(ByVal record As IDataRecord)

    _employeeID = CType(record("EmployeeID"), Integer)
    _firstName = CType(record("FirstName"), String)
    _lastName = CType(record("LastName"), String)
    _title = CType(record("Title"), String)
    _hireDate = CType(record("HireDate"), DateTime)
    CreatePhoto(CType(record("Photo"), Byte()))

  End Sub
 
  ' BitmapImage creation
  Private  Sub CreatePhoto(ByVal photoSource() As Byte)

    ' Create the bitmap object
    ' NOTE: This is *not* a GDI+ Bitmap object
    _photo = New BitmapImage()
    Dim strm As MemoryStream =  New MemoryStream() 
 
    ' Well-known hack to make Northwind images work
    Dim offset As Integer =  78 
    strm.Write(photoSource, offset, photoSource.Length - offset)
 
    ' Read the image into the bitmap object
    _photo.BeginInit()
    _photo.StreamSource = strm
    _photo.EndInit()
 
  End Sub
End Class

该类接受一个 IDataRecord 类(DataReader 的单一结果,不过我们马上就会对此进行介绍),并填写我们在本文前面的 DataTable 示例中使用的那些字段。请注意,我们已经将此处的 BitmapImage 创建移至业务对象,从而可以在 UI 类中更简单地使用雇员。

接下来,我们将需要这些字段的属性访问器:

在 C# 中:

// Read-Only
public int EmployeeID
{
  get { return _employeeID; }
}

public string FirstName
{
  get { return _firstName; }
  set { _firstName = value; }
}

public string LastName
{
  get { return _lastName; }
  set { _lastName = value; }
}

public string Title
{
  get { return _title; }
  set { _title = value; }
}

public DateTime HireDate
{
  get { return _hireDate; }
  set { _hireDate = value; }
}

// Read-Only
public BitmapImage Photo
{
  get { return _photo; }
}

在 Visual Basic .NET 中:

' Read-Only
Public ReadOnly Property EmployeeID() As Integer
  Get 
     Return _employeeID
  End Get
End Property
 
Public Property FirstName() As String
  Get 
     Return _firstName
  End Get
  Set (ByVal Value As String) 
     _firstName = value
  End Set
End Property
 
Public Property LastName() As String
  Get 
     Return _lastName
  End Get
  Set (ByVal Value As String) 
     _lastName = value
  End Set
End Property
 
Public Property Title() As String
  Get 
     Return _title
  End Get
  Set (ByVal Value As String) 
     _title = value
  End Set
End Property
 
Public Property HireDate() As DateTime
  Get 
     Return _hireDate
  End Get
  Set (ByVal Value As DateTime) 
     _hireDate = value
  End Set
End Property
 
' Read-Only
Public ReadOnly Property Photo() As BitmapImage
  Get 
     Return _photo
  End Get
End Property

在这些代码中,我们仅允许对类中的字段进行读写(或只读)访问。现在,可以编写一个集合来保存我们的雇员:

在 C# 中:

public class EmployeeList : ObservableCollection
{
  public EmployeeList()
  {
    string connString =
           ConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
    string query = @"
      SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo 
      FROM Employees
    ";

    // Fill the Set with the data
    using (SqlConnection conn = new SqlConnection(connString))
    {
      try
      {
        SqlCommand cmd = conn.CreateCommand();
        cmd.CommandText = query;

        conn.Open();
        SqlDataReader rdr = cmd.ExecuteReader();
        while (rdr.Read())
        {
          Add(new Employee(rdr));
        }
      }
      finally
      {
        if (conn.State != ConnectionState.Closed) conn.Close();
      }
    }
  }
}

在 Visual Basic .NET 中:

Public Class EmployeeList
   Inherits ObservableCollection
  Public  Sub New()
    String connString =
           ConfigurationManager.ConnectionStrings("Northwind").ConnectionString

    String query = _
      "SELECT EmployeeID, FirstName, LastName, Title, HireDate, Photo " + _
      "  FROM Employees"

    ' Fill the Set with the data
    Using conn as New SqlConnection(connString)
    
      Try
        Dim cmd As SqlCommand =  c