Windows Presentation Foundation 数据绑定

第一部分

本页内容

简介简介
简化的 XAML 绑定简化的 XAML 绑定
我们所处的位置我们所处的位置
参考资料参考资料

简介

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 提要的编辑器,允许用户查看和编辑提要。


图 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)。


图 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 所示。


图 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 所示。


图 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 所示的窗体。


图 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 所示。


图 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 XPath=@ID}" /> 
  <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 表达式。

 

第二部分

 

本页内容

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

简介

社区中大多数有争议的 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 所示的界面:


图 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>

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


图 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 =  conn.CreateCommand() 
        cmd.CommandText = query
 
        conn.Open()
        Dim rdr As SqlDataReader =  cmd.ExecuteReader() 
        While rdr.Read()
          Add(New Employee(rdr))
        End While
      Finally
        If conn.State <> ConnectionState.Closed Then
            conn.Close()
        End If
      End Try
    
    End Using

  End Sub
End Class

该集合的基类是 ObservableCollection 类,它提供一种机制,使得 UI 可以知道该集合中是否添加了新成员。我们已经将数据访问从 UI 页移至 Collection 类。创建该类后,我们查询数据库并通过 DataReader 向该集合添加新雇员。既然我们具有了集合和单个对象,就可以通过映射将类导入 XAML 中(本系列文章的第一部分对此进行了详细解释):

<Window 
     ... 
     xmlns:e="Example" 
     DataContext="{StaticResource EmployeeList}" 
     > 
  <Window.Resources> 
     <e:EmployeeList x:Key="EmployeeList" /> 
     ... 
  </Window.Resources>
  ... 
</Window> 

我们用 ?Mapping 声明将该类导入 XAML 文档中,并在 Resources 中指定 EmployeeList,这样我们就可以将其用作窗口的 DataContext。这样,XAML 文件的其余部分就与原始雇员浏览器完全相同了,因为我们仍将尝试在 DataSet 示例中使用的那些字段名。唯一的改动是绑定 XAML 文档中的 BitmapImage,而不是在隐藏代码中进行该操作:

... 
   <Image Name="theImage" Canvas.Top="5" Canvas.Left="5" Width="75"
             Source="{Binding Path=Photo}"
   /> ... 

现在,我们具有一个行为相同的雇员浏览器:


图 3. 基于业务对象的雇员浏览器

除了使用类型映射,您还可以使用 ObjectDataProvider 将对象置于 XAML 中。正如我在本文第一部分中介绍的那样,只需指定一个键和类型名称:

<ObjectDataProvider x:Key="EmployeeList" 
                                   TypeName="Example.Data.EmployeeList, ExampleCS"/>

x:Key 只是一个要在绑定中使用的名字对象,Typename 是类名和程序集(在本例中是我们的 UI 所在的程序集)。XAML 的其余部分保持不变,因为我们要加载相同的数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值