[转载]让人迷恋的 WPF 数据绑定
绑定是要点所在

[转载]让人迷恋的 WPF 数据绑定
更好的绑定

[转载]让人迷恋的 WPF 数据绑定
绑定到复数数据

[转载]让人迷恋的 WPF 数据绑定
自定义数据绑定样式

[转载]让人迷恋的 WPF 数据绑定
跟踪集合更改

[转载]让人迷恋的 WPF 数据绑定
我们所处的位置

“任何使用 Avalon 的人如果不使用数据绑定,一定会发疯。”

–Mark Boulter 2004 年 6 月 2 日

我热爱我选择的生活方式,因为我花费一大部分时间来进行学习。当我学习新东西时,我从来不会对大脑中突然蹦出的“灵感”感到厌烦。最近我的大脑中就出现过这样一个灵感,它促使我从根本上重新考虑我编写用户界面的方法。下面是一个表明我原来做法的简单示例:

class MyForm : Form {
  Game game1;
  StatusBar statusBar1;
  ...
  void InitializeComponent() {
    ...
    this.game1.ScoreChanged +=
      new EventHandler(this.game1_ScoreChanged);
    ...
  }

  void game1_ScoreChanged(object sender, EventArgs e) {
    this.statusBar1.Text = "Score: " + this.game1.Score;
  }
}

在上述代码中,我拥有一个窗口,其中含有一个类型为 Game 的自定义组件,该组件具有一个 Score 属性,当该属性更改时将引发 ScoreChanged 事件。代码将捕获该事件,使用新的数据格式化一个字符串,然后在状态栏中显示它。这很不错,因为组件不必知道有关谁在侦听其属性更改的任何信息,同时窗口可以对数据执行它喜欢的任何操作。

绑定是要点所在

在 Windows 窗体中,我可以将此向前推进一步,即使用数据绑定将 Score 更改通知直接挂钩到状态栏控件:

class MyForm : Form {
  Game game1;
  StatusBar statusBar1;
  ...
  public MyForm() {
    ...
    statusBar1.DataBindings.Add("Text", game1, "Score");
  }
  ...
}

在该例中,由于 ScoreChanged 事件所使用的命名约定 (Changed),Windows 窗体可以及时注意到 Score 属性的更改并直接设置状态栏控件的 Text 属性。然而,该方案中缺少的是将数据与“Score:”前缀进行合成的机会。要获得该功能,我们必须处理 Binding 对象上的 Format 事件,该事件是通过调用 DataBindings 集合的 Add 方法创建的:

public MyForm() {
  ...
  Binding binding =
    statusBar1.DataBindings.Add("Text", game1, "Score");
  binding.Format +=
    new ConvertEventHandler(binding_Format);
}

void binding_Format(object sender, ConvertEventArgs e) {
  e.Value = "Score: " + e.Value.ToString();
}

此刻,我们已经将 Game 对象的 Score 属性组合为我们所需要的字符串,但是我们将三行代码分布到两个方法中,使其变得有一点儿难以理解。另一方面,我发现 Avalon 的数据绑定语法更为简洁一些,尤其是在使用 XAML 时:

<!-- MyWindow.xaml -->
<Window ... Loaded="MyWindow_Loaded" >
  <FlowPanel>
    <Text TextContent="Score: " />
    <Text TextContent="*Bind(Path=Score)" />
  </FlowPanel>
</Window>

在该 XAML 数据中,请注意我用 XAML 并通过 FlowPanel 声明了一个状态栏(您可以重新阅读我的上一篇文章以复习一下 FlowPanel — 它可将任意数量的不同种类的内容汇聚到一起)。FlowPanel 将两段文本汇聚到一起 — 一个固定的字符串和一个可变的分数 — 就像上述 Windows 窗体示例一样。不同之处在于,我不是编写一段命令式的代码来创建状态栏的完整内容,而是将文本段声明为 UI 本身的一部分。可变文本来自使用 Score 属性的路径 进行的绑定。Avalon 中的绑定是一块 UI 到一块数据的映射。在该例中,即设置我们要定义的 Text 对象的 TextContent 属性。路径是有关如何获取数据的说明。在该例中,即 Score 属性。

如果该 *Bind 语法在您看起来很奇怪,您应该知道 XAML 在设计时考虑了手动创作,因此所生成的语法有助于节省击键操作(这是 XML 大体上不具有的特点)。如果您愿意使用更加繁琐的方法,可以使用 XAML 的复合属性语法 来创建 Bind 对象。通过复合属性语法,可以使用点分名称将属性设置为元素,从而将父元素名和属性名组合为它自己的元素,如下所示:

<Window ... Loaded="MyWindow_Loaded" >
  ...
  <Text TextContent="Score: " />
  <Text>
    <Text.TextContent>      <!-- compound property syntax -->
      <Bind Path="Score" /> <!-- expanded Bind syntax -->
    </Text.TextContent>
  </Text>
</Window>

因此,我们将对象的 Score 属性绑定到 Text 控件的 TextContent 属性,但是具有 Score 属性的对象来自何处?该对象通过 DataContext 属性进行设置:

partial class MyWindow : Window {
  void MyWindow_Loaded(object sender, EventArgs e) {
    Game game = ((SolApp)SolApp.Current).Game;
    this.DataContext = game;
  }
  ...
}

数据绑定沿控件层次结构向上进行,因此当 Text 控件将其 TextContent 属性数据绑定到 Score 属性时,Avalon 会向上进行挖掘以查找有效的数据上下文。如果我希望缩小数据上下文的范围,我可以设置该特定 Text 控件的 DataContext 属性。我选择了窗口的数据上下文,以防我可能希望让其他控件绑定到 Game 对象的其他属性(就像计时游戏的 Time 属性一样)。

使得我的大脑中迸发这一灵感的事情是,在我原来的思维方式中,我具有三个部分:数据、UI 以及二者之间的映射代码。在新的方法中,我只有数据和 UI,而不必编写任何映射代码。与原来不同的是,UI 本身能够决定它要显示数据的哪些部分以及如何显示。这将具有非常重要的意义。

[转载]让人迷恋的 WPF 数据绑定返回页首

更好的绑定

绑定到单个对象上的单个属性是很有趣的,但是让我们尝试某种稍微复杂一点儿的做法。例如,设想有一个类,它具有两个公共的读写属性:

public class Person {
  string name;
  public string Name {
    get { return this.name; }
    set { this.name = value; }
  }

  int age;
  public int Age {
    get { return this.Age; }
    set { this.Age = value; }
  }

  public Person(string name, int age) {
    this.name = name;
    this.age = age;
  }
}

注意,如果我们采取捷径,使 NameAge 成为公共字段以便简化代码,则 Avalon 不会绑定到它们。Avalon 只会绑定到公共属性。绑定到 Person 对象的一个实例时,将如下所示:

<!-- Window1.xaml -->
<Window ... >
    <GridPanel Columns="2">
      <Text>Name</Text>
      <TextBox Text="*Bind(Path=Name)"/>
      <Text>Age</Text>
      <TextBox Text="*Bind(Path=Age)"/>
      <Border />
      <Button ID="showButton">Show</Button>
      <Border />
      <Button ID="birthdayButton">Birthday</Button>
    </GridPanel>
</Window>

// Window1.xaml.cs
...
partial class Window1 : Window {
  Person person = new Person("John", 10);

  void Window1_Loaded(object sender, EventArgs e) {
    this.DataContext = this.person;
    showButton.Click += showButton_Click;
    birthdayButton.Click += birthdayButton_Click;
  }

  void showButton_Click(object sender, ClickEventArgs e) {
    MessageBox.Show(
      string.Format(
        "Person= {0}, age {1}",
        this.person.Name, this.person.Age));
  }

  void birthdayButton_Click(object sender, ClickEventArgs e) {
    ++this.person.Age;
  }
}

运行该应用程序并按 Show 按钮时,将产生意料之中的图 1。

[转载]让人迷恋的 WPF 数据绑定

1. 显示被数据绑定的 Person 对象

同样,因为我们不仅从对象中读取数据,而且还允许写入。更改年龄文本框,并按 Show 按钮以显示我们已经将 TextBox 控件绑定到的 Person 对象的当前状态时,将展现图 2。

[转载]让人迷恋的 WPF 数据绑定

2. 显示更新后的 Person 对象

图 2 显示我们正在两个方向进行绑定。即,从数据到文本框。而随着文本框的变化,发生的更改将被复制回基础对象。

然而,就 Person 类的当前实现而言,尽管我们的 Birthday 按钮实现更改了基础对象,但它将不会导致 UI 更新。换句话说,连续按 BirthdayShow 按钮将导致如图 3 所示的差异。

[转载]让人迷恋的 WPF 数据绑定

3. 显示未正确启用双向数据绑定的已更新 Person 对象

问题在于,尽管 Avalon 数据绑定引擎可以监控 UI 更改并更新基础对象数据,但该对象本身在其数据被直接更改时并不会引发任何事件。那么,我们该怎么办呢?要使 Avalon 跟踪 Person 类实例上发生的更改,它需要实现 IPropertyChange 接口:

namespace System.ComponentModel {
  public interface IPropertyChange {
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

Updating our Person to support IPropertyChange looks like this:

class Person : IPropertyChange {
  public event PropertyChangedEventHandler PropertyChanged;
  void FirePropertyChanged(string propertyName) {
    if( this.PropertyChanged != null ) {
      PropertyChanged(this,
        new PropertyChangedEventArgs(propertyName));
    }
  }

  string name;
  public string Name {
    get { return this.name; }
    set {
      this.name = value;
      FirePropertyChanged("Name");
    }
  }

  int age;
  public int Age {
    get { return this.age; }
    set {
      this.age = value;
      FirePropertyChanged("Age");
    }
  }
  ...
}

当 Avalon 绑定到 Person 对象时,它将预订 PropertyChanged 事件,以便它能够在属性更改时更新绑定到这些属性的任何控件。在我们的 Person 类中,我们在任何属性更改时引发了该事件,以确保指定发生更改的属性的名称。通过这种方式,无论 UI 更改还是对象更改,这两者都能保持同步,而我们无须在两者之间编写代码以使事情恢复正常。

如果您熟悉支持 Windows 窗体数据绑定的 Changed 事件,则可以使用 Avalon 的 IPropertyChange 接口来取代该约定。因为所有属性更改通知都通过单个事件引发,所以 Avalon 的机制可能更为有效。然而,在当前版本中,Avalon 不能识别 Windows 窗体 Changed 事件,因此,已经实现这些事件的对象必须添加对 Avalon 的新方法的支持,该新方法提供了属性更改通知。

[转载]让人迷恋的 WPF 数据绑定返回页首

绑定到复数数据

迄今为止,我已经向您说明了两个绑定到单个对象的示例。因为数据绑定与 XAML 之间存在紧密的集成,所以这种风格的绑定是自然和灵活的做法。但是,更为传统的绑定手段是绑定到一系列项目:

<!-- Window1.xaml -->
<Window ... >
    <GridPanel Columns="2">
      <Text>Persons</Text>
      <ListBox ItemsSource="*Bind()" />
      ...
    </GridPanel>
</Window>

// Window1.xaml.cs
...
public partial class Window1 : Window {
  ArrayList persons = new ArrayList();

  void Window1_Loaded(object sender, EventArgs e) {
    persons.Add(new Person("John", 10));
    persons.Add(new Person("Tom", 8));
    this.DataContext = this.persons;
    ...
  }
  ...
}

在该例中,我们已经将数据上下文设置为 Person 对象的数组列表。为了将 ListBox 控件绑定到该数据,对于 ItemsSource 属性我们只是使用 *Bind(),而未指定 Path,因为我们希望在各个项目中表示整个对象。默认情况下,将显示每个 Person 对象,如图 4 所示。

[转载]让人迷恋的 WPF 数据绑定

4. 以令人不愉快的方式显示一系列 Person 对象

如果您熟悉 Windows 窗体数据绑定,您将会认识到显示的是每个对象的类型,而不是有意义的值。默认情况下,将调用 Person 类的 ToString 方法来获取每个对象的字符串表示,从而产生返回类型名的 Object 基类方法实现。

Windows 窗体提供了多种方法来解决该问题,范围涉及选择单个显示属性到覆盖 Person 类的 ToString 方法。Avalon 数据绑定倾向于另一种技术,即使用样式 来决定应该如何显示 Person 对象。

[转载]让人迷恋的 WPF 数据绑定返回页首

自定义数据绑定样式

要定义 Avalon 中列表框项目的名称,我们不使用所有者绘制或自定义绘制,而是使用成分。列表框中的每个项目都是一个或多个 UI 元素的成分,并且根据各个项目的数据绑定值按需产生。进入各个项目的元素列表由 Avalon 样式 提供。您可以将样式视为充当元素及其属性的初始描述的模板或复印。

作为您可能希望对样式进行的处理的简单示例,请设想将每个按钮的文本设置为粗体。一种完成该任务的方法是设置每个 Button 元素的 FontWeight 属性:

<Window ... > 
  <Button FontWeight="Bold" ID="showButton">Show</Button>
  ...
  <Button FontWeight="Bold" ID="birthdayButton">Birthday</Button>
  ...
</Window>

当然,该方法的问题与所有复制-粘贴软件构建手段相同:可维护性。将每个按钮设置为粗体的一种更加健壮的方法是定义按钮的样式,例如:

<Window  
    xmlns="http://schemas.microsoft.com/2003/xaml"
    xmlns:def="Definition"
    def:Class="PersonBinding.Window1"
    def:CodeBehind="Window1.xaml.cs" 
    Text="PersonBinding"
    Loaded="Window1_Loaded"
    >
  <Window.Resources>
    <Style>
      <Button FontWeight="Bold" />
    </Style>
  </Window.Resources>
  ...
  <!-- this button will be bold -->
  <Button ID="showButton">Show</Button>
  ...
  <!-- this button will also be bold -->
  <Button ID="birthdayButton">Birthday</Button>
  ...
</Window>

这里,我们已经在按钮的包含窗口的 Resources 区域内部定义了一种样式。像数据上下文一样,样式也是按层次组织的,因此在创建一个按钮时,将遍历其父样式(以及更高层的样式)来查找要应用的样式。样式本身将充当模板,设置在该窗口中创建的所有按钮对象的 FontWeight 属性。

如果要进一步采用样式,可以向其赋予名称,并使用 def:Name 属性选择性地应用它们,如下所示:

<Window ... >
  <Window.Resources>
    <Style def:Name="BoldButton">
      <Button FontWeight="Bold" />
    </Style>
  </Window.Resources>
  ...
  <!-- this button will be bold -->
  <Button ID="showButton" Style="{BoldButton}">Show</Button>
  ...
  <!-- this button will not be bold -->
  <Button ID="birthdayButton">Birthday</Button>
  ...
</Window>

在该例中,按钮样式是相同的,但它被赋予了一个名称,该名称被应用于(使用特殊的大括号语法)我们希望将其变为粗体的按钮的 Style 属性。这只是 Avalon 样式的冰山一角(有关详细信息,请参见 Longhorn SDK),但对于要构建列表框样式的我们来说已经足够了:

<Window ... >
  <Window.Resources>
    <Style def:Name="PersonStyle">
      <Style.VisualTree>
        <FlowPanel>
          <Text TextContent="*Bind(Path=Name)" />
          <Text TextContent=":" />
          <Text TextContent="*Bind(Path=Age)" />
          <Text TextContent=" years old" />
        </FlowPanel>
      </Style.VisualTree>
    </Style>
  </Window.Resources>
  <GridPanel Columns="2">
    <Text>Persons</Text>
    <ListBox ItemStyle="{PersonStyle}" ItemsSource="*Bind()" />
    ...
  </GridPanel>
</Window>

注意 PersonStyle 样式,它由一组文本控件组成,其中一些控件带有使用常量字符串设置的文本内容,而另一些控件则使用数据绑定。在当前版本的 Longhorn 中,应用列表框项目样式的最简单方法是命名该样式并将其作为 ListBox 控件的 ItemStyle 属性进行应用。不必影响基础 Person 类,我们便可自定义 Person 对象的视图,如图 5 中所示。

[转载]让人迷恋的 WPF 数据绑定

5. 以令人略感愉快的方式显示一系列 Person 对象

既然我们可以看到列表框中的项目,很明显文本框反映了当前的列表框选择。这由在列表框和文本框之间共享的视图进行管理。除了当前状态以外,该视图还管理筛选和排序。在本文章系列的下一篇文章中,将对此进行更为深入的讨论。

[转载]让人迷恋的 WPF 数据绑定返回页首

跟踪集合更改

现在要讨论的另外一件事情是管理集合本身的更改。例如,如果我要在小示例应用程序中创建一个 Add 按钮,我可能选择按以下方式来实现它:

void addPersonButton_Click(object sender, ClickEventArgs e) {
  this.persons.Add(new Person("Chris", 34));
}

这里的问题是我们的数据绑定控件根本不会意识到这一更改。就像数据绑定对象 需要实现 IPropertyChange 接口一样,数据绑定列表 需要实现 ICollectionChange 接口:

namespace System.ComponentModel {
  public interface ICollectionChange {
    public event CollectionChangeEventHandler CollectionChanged;
  }
}

ICollectionChange 接口用于通知数据绑定控件已经在绑定列表中添加或删除了项目。尽管常见的做法是在自定义类型中实现 IPropertyChange,以支持在类型属性上进行双向数据绑定,但除非您要实现自己的集合类,否则您不必实现 ICollectionChange 接口。相反,您很可能依赖于 .NET 框架类库中的一个集合类来为您实现 ICollectionChange。遗憾的是,目前只有极少数类实现了 ICollectionChange,而我们要用来存放 Person 对象的类 (ArrayList) 不属于这些类。幸亏 Avalon 提供了 ArrayListDataCollection 类专门用于此目的:

namespace System.Windows.Data {
  public class ArrayListDataCollection :
    ArrayList, ICollectionChange, ... {...}
}

因为 ArrayListDataCollection 类派生于 ArrayList 并且实现了 ICollectionChange 接口,所以每当需要支持数据绑定的 ArrayList 时,都可以使用它。

partial class Window1 : Window {
  ArrayListDataCollection persons = new ArrayListDataCollection();

  void Window1_Loaded(object sender, EventArgs e) {
    persons.Add(new Person("John", 10));
    persons.Add(new Person("Tom", 8));
    this.DataContext = this.persons;
    ...
  }
  ...
}

现在,当从 persons 列表中删除一个项目时,相应的更改将反映在数据绑定控件中。

令人鼓舞的是,尽管 Avalon 数据绑定不像 Windows 窗体那样支持 Changed 约定,但与 Windows 窗体数据绑定接口 ICollectionChange 等效的 IBindingList 接口受到 Avalon 的支持。还有一个额外的好处 — 因为 IBindingList 提供了 ICollectionChangeIPropertyChange 的功能,所以任何目前接通 Windows 窗体数据绑定的数据源对于 Avalon 数据绑定也将完全有效(这包括 ADO.NET 中的 DataTable 对象等)。

[转载]让人迷恋的 WPF 数据绑定返回页首

我们所处的位置

我从讨论游戏和分数以及它们如何将我的思想导向 Avalon 中的数据绑定开始。我们一开始讨论了绑定到对象的基础知识,以及如何使用 IPropertyChange 在对象和文本框控件之间实现双向更改通知。我向您介绍了简洁的、扩展的绑定语法,并且随后继续讨论了如何绑定到数据列表,如何设置列表项的样式以及如何使用 ICollectionChange 跟踪双向列表更改。

正如文中所表明的那样,对于 Avalon 中的数据绑定有大量相关内容。在下一期中,我将讨论其他数据绑定主题(如用于高级数据样式设置的转换器和样式选择器、自定义视图和筛选器),并且插入一些拖放操作来推进我的 solitaire 实现,如图 6 所示。

[转载]让人迷恋的 WPF 数据绑定

6. 我的 Solitaire 应用程序被更新为使用数据绑定

图 6 中显示的所有数据都是使用数据绑定实现的,包括全部七堆纸牌和分数。而且正如 Mark 所说的,如果我用其他任何方式实现它,那我一定会发疯。

致谢

首先必须感谢 Namita Gupta 和 David Jenni — Avalon 数据绑定的项目经理和首席开发人员。Namita 不仅在 PDC 极为成功的发表了有关 Avalon 数据绑定的讲话,而且她和 David 还非常出色地回答了我的数据绑定问题,并帮助我处理了当前版本中存在的问题。

还要感谢 Lutz Roeder 提供了在 Longhorn 上运行的 Reflector 版本。它是一个非常宝贵的工具,用于填充尚未记录的详细信息。坦白地说,如果没有这一工具,我不知道现在从事 Longhorn 开发的人们将如何生存。谢谢你,Lutz!

参考资料

Avalon Data Binding in the Longhorn SDK

相关文章:

  • 2022-12-23
  • 2021-12-31
  • 2021-11-05
  • 2022-02-07
猜你喜欢
  • 2021-04-15
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-10-17
相关资源
相似解决方案