Chris Sells
Microsoft Corporation
摘要:Chris Sells 继续讨论 Avalon,并且将数据绑定引入其基于 Longhorn 的 Solitaire 应用程序。
下载 PersonBinding.msi 示例文件。
绑定是要点所在 | |
更好的绑定 | |
绑定到复数数据 | |
自定义数据绑定样式 | |
跟踪集合更改 | |
我们所处的位置 |
“任何使用 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 本身能够决定它要显示数据的哪些部分以及如何显示。这将具有非常重要的意义。
绑定到单个对象上的单个属性是很有趣的,但是让我们尝试某种稍微复杂一点儿的做法。例如,设想有一个类,它具有两个公共的读写属性:
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; } }
注意,如果我们采取捷径,使 Name 和 Age 成为公共字段以便简化代码,则 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。
同样,因为我们不仅从对象中读取数据,而且还允许写入。更改年龄文本框,并按 Show 按钮以显示我们已经将 TextBox 控件绑定到的 Person 对象的当前状态时,将展现图 2。
图 2 显示我们正在两个方向进行绑定。即,从数据到文本框。而随着文本框的变化,发生的更改将被复制回基础对象。
然而,就 Person 类的当前实现而言,尽管我们的 Birthday 按钮实现更改了基础对象,但它将不会导致 UI 更新。换句话说,连续按 Birthday 和 Show 按钮将导致如图 3 所示的差异。
问题在于,尽管 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 的新方法的支持,该新方法提供了属性更改通知。
迄今为止,我已经向您说明了两个绑定到单个对象的示例。因为数据绑定与 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 所示。
如果您熟悉 Windows 窗体数据绑定,您将会认识到显示的是每个对象的类型,而不是有意义的值。默认情况下,将调用 Person 类的 ToString 方法来获取每个对象的字符串表示,从而产生返回类型名的 Object 基类方法实现。
Windows 窗体提供了多种方法来解决该问题,范围涉及选择单个显示属性到覆盖 Person 类的 ToString 方法。Avalon 数据绑定倾向于另一种技术,即使用样式 来决定应该如何显示 Person 对象。
要定义 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 中所示。
既然我们可以看到列表框中的项目,很明显文本框反映了当前的列表框选择。这由在列表框和文本框之间共享的视图进行管理。除了当前状态以外,该视图还管理筛选和排序。在本文章系列的下一篇文章中,将对此进行更为深入的讨论。
现在要讨论的另外一件事情是管理集合本身的更改。例如,如果我要在小示例应用程序中创建一个 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 提供了 ICollectionChange 和 IPropertyChange 的功能,所以任何目前接通 Windows 窗体数据绑定的数据源对于 Avalon 数据绑定也将完全有效(这包括 ADO.NET 中的 DataTable 对象等)。
我从讨论游戏和分数以及它们如何将我的思想导向 Avalon 中的数据绑定开始。我们一开始讨论了绑定到对象的基础知识,以及如何使用 IPropertyChange 在对象和文本框控件之间实现双向更改通知。我向您介绍了简洁的、扩展的绑定语法,并且随后继续讨论了如何绑定到数据列表,如何设置列表项的样式以及如何使用 ICollectionChange 跟踪双向列表更改。
正如文中所表明的那样,对于 Avalon 中的数据绑定有大量相关内容。在下一期中,我将讨论其他数据绑定主题(如用于高级数据样式设置的转换器和样式选择器、自定义视图和筛选器),并且插入一些拖放操作来推进我的 solitaire 实现,如图 6 所示。
图 6 中显示的所有数据都是使用数据绑定实现的,包括全部七堆纸牌和分数。而且正如 Mark 所说的,如果我用其他任何方式实现它,那我一定会发疯。
致谢
首先必须感谢 Namita Gupta 和 David Jenni — Avalon 数据绑定的项目经理和首席开发人员。Namita 不仅在 PDC 极为成功的发表了有关 Avalon 数据绑定的讲话,而且她和 David 还非常出色地回答了我的数据绑定问题,并帮助我处理了当前版本中存在的问题。
还要感谢 Lutz Roeder 提供了在 Longhorn 上运行的 Reflector 版本。它是一个非常宝贵的工具,用于填充尚未记录的详细信息。坦白地说,如果没有这一工具,我不知道现在从事 Longhorn 开发的人们将如何生存。谢谢你,Lutz!
参考资料
• | Avalon Data Binding in the Longhorn SDK |
• | Using Data in Your "Avalon" Application:Data Visualization, Databinding and Integration with WinFS and Databases, Namita Gupta, Microsoft Professional Developer Conference 2003, Session CLI306(幻灯片和代码示例) |
• | Another Step Down the Longhorn Road, Chris Sells, April 12, 2004 |
• | Avalon Styles Overview in the Longhorn SDK |
• | Reflector for .NET, Lutz Roeder |
Chris Sells 是 MSDN Online 的内容战略家,当前专注于研究 Longhorn(Microsoft 的下一个操作系统)。他已撰写了若干部著作,包括 Mastering Visual Studio .NET 和 Windows Forms Programming in C#。在业余时间内,Chris 主持各种会议,指导 Genghis 可用源项目,和 Rotor 休闲娱乐,并且通常会在 blogsphere 中捣捣乱。有关 Chris 及其各种项目的详细信息,请访问 http://www.sellsbrothers.com。
转到原英文页面
返回页首 |