我们希望能够直接将对象和对象的集合绑定到 Avalon UI 元素。作为一个示例,以下代码显示了我们用于探究绑定在 Avalon 中数据的 Person 类。
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"); } } ... }
IPropertyChange 接口由 Person 类实现,以通知绑定到实例的任意控件,其中一个属性已经更改。相反,公共属性让绑定控件的数据可以访问每个属性的当前值,并应用 UI 中发起的变化。图 1 中的 Name 和 Age TextBox 控件显示了 Person 对象的一个实例,该对象绑定到每个控件的 TextContent 属性。
图 1. 管理使用 Avalon 数据绑定的 Person 对象的列表
返回页首
当前项目
在 Name 和 Age TextBox 控件绑定到单个对象时,Persons ListBox 控件绑定到 Person 对象的集合中。由于 ListBox 中的选择发生了变化,当前项目 也发生变化,所有绑定控件的数据按照它们认为合适的方式进行处理。例如,如图 1 所示,通过突出显示其列表中的对象,ListBox 反映了当前项目,同时 TextBox 将仅显示当前项目的绑定属性值。当前,跟踪哪个项目是由数据的视图 来管理的。视图是一个位于数据和共享数据视图的控件之间的对象,管理着像当前项目、筛选和排序这样的操作。实际上,在 Avalon 中,完全不需要绑定到数据,而是使用程序员或 Avalon 提供的数据视图。
例如,以下代码显示了如何使用默认视图来更新上一篇文章中的 Show 按钮实现,以显示当前选定的项目:
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; showButton.Click += showButton_Click; birthdayButton.Click += birthdayButton_Click; addPersonButton.Click += addPersonButton_Click; } void showButton_Click(object sender, ClickEventArgs e) { ListCollectionView view = (ListCollectionView)Binding.GetView(persons); Person person = (Person)view.CurrentItem.Current; MessageBox.Show( string.Format("Name is '{0}' and you are {1} years old", person.Name, person.Age)); } ... }
这个 Show 按钮单击处理程序代码调用 Binding 对象上的静态 GetView 方法,该对象会返回与 person 数据相关联的默认视图。回忆 persons 字段是 ArrayListDataCollection 的实例(您将会想到我的上一篇文章),它是 ArrayList 类的子类,该类添加 ICollectionChange 接口的实现,以便绑定到集合的控件(如 Persons ListBox)可以注册集合本身更改时的通知。
如果已经获得要绑定的项目集合,从 GetView 方法返回的视图对象的类型将成为 ListCollectionView 类的派生,它将进一步向下延续基类 CollectionView 的继承链:
namespace System.Windows.Data { public class ListCollectionView : ContextAffinityCollectionView, ICurrentItem, IComparer { public override SortDescription[] Sort { get; set; } public override bool Contains(object item); public ListCollectionView(System.Collections.IList list); public override int Count { get; } public override void Refresh(); public override bool ContainsItem(object item); public override IEnumerator GetEnumerator(); public override int IndexOf(object item); public IContains CustomFilter { get; set; } public override bool CanSort { get; } public IComparer CustomSort { get; set; } } public abstract class ContextAffinityCollectionView : CollectionView { } } namespace System.ComponentModel { public abstract class CollectionView : IEnumerable, ICollectionChange { public virtual ICurrentItem CurrentItem { get; } ... } }
当用户更改绑定 ListBox 中的选择时,CollectionView 基类中的 CurrentItem 属性发生变化,然后绑定控件的其他数据使用该属性来显示它们的内容。图 2 显示了这种关系。
图 2. 项目、当前项目、视图和绑定控件
该视图还用于比只维护当前项目更不常用的任务,例如排序和筛选。
返回页首
排序
由于视图始终位于绑定控件的数据和数据本身之间。这意味着可能会贸然出现我们不希望显示的数据(这称为筛选,且它将被直接覆盖),并且可能会更改数据显示的顺序(排序)。最简单的排序方法就是设置视图的 Sort 属性:
void sortButton_Click(object sender, ClickEventArgs e) { ListCollectionView view = (ListCollectionView)Binding.GetView(persons); if( view.Sort.Length == 0 ) { view.Sort = new SortDescription[] { new SortDescription("Name", ListSortDirection.Ascending), new SortDescription("Age", ListSortDirection.Descending), }; } else { view.Sort = new SortDescription[0]; } view.Refresh(); }
请注意由要进行排序的属性名称和顺序(升序或降序)构建的 SortDescription 对象数组的使用。还要注意对视图对象上的 Refresh 的调用。当前,这要求使用视图的新属性来刷新绑定控件(尽管希望在 Longhorn 的将来的版本中不要求对 Refresh 显式调用)。
SortDescription 对象的数组应该涵盖大多数情况,但是如果想要更多的控件,可以通过实现 IComparer 接口为视图提供自定义排序对象。
void sortButton_Click(object sender, ClickEventArgs e) { ListCollectionView view = (ListCollectionView)Binding.GetView(persons); if( view.CustomSort == null ) { view.CustomSort = new PersonSorter(); } else { view.CustomSort = null; } view.Refresh(); } class PersonSorter : IComparer { public int Compare(object x, object y) { Person lhs = (Person)x; Person rhs = (Person)y; // Sort Name ascending and Age descending int nameCompare = lhs.Name.CompareTo(rhs.Name); if( nameCompare != 0 ) return nameCompare; int ageCompare = 0; if( lhs.Age < rhs.Age ) ageCompare = -1; else if( lhs.Age > rhs.Age ) ageCompare = 1; return ageCompare; } }
这个自定义排序实现碰巧与以前排序说明的集合具有相同的行为,但您可以完成任何想要进行的操作来确定对象在数据绑定控件中的存储方式。此外,将视图的 Sort 属性设置为 SortDescription 对象的空数组,并且将视图的 CustomSort 属性设置为 null,可以关闭排序。
返回页首
筛选
仅仅因为所有对象按照令您高兴的某个顺序显示并不意味着您希望显示所有对象。对于出现在数据中但不属于该视图的那些恶意对象,我们需要为视图提供一个 IContains 接口的实现:
void filterButton_Click(object sender, ClickEventArgs e) { ListCollectionView view = (ListCollectionView)Binding.GetView(persons); if( view.CustomFilter == null ) { view.CustomFilter = new PersonFilter(); } else { view.CustomFilter = null; } view.Refresh(); } class PersonFilter : IContains { public bool Contains(object item) { Person person = (Person)item; // Filter adult Persons return person.Age >= 18; } }
这种筛选实现仅筛选成年人,但自定义筛选对象可以完成您想做的所有操作。同样,将视图的 CustomFilter 属性设置为 null 可以关闭筛选。
返回页首
转换程序
排序和筛选是处理控件显示数据方式的两个非常有用的方法。但是,如果我们希望对数据进一步操作,而不仅仅是将其作为字符串显示,又该如何呢?例如,设想我们希望根据要显示的 Person 对象的年龄来更改 ListBox 中项目的颜色。恢复我们用于显示 ListBox 中每个 Person 对象的样式:
注意,这段代码绑定到 Text 元素的 TextContent 属性,而 Text 元素则构成了用于从列表框中呈现项目的 PersonStyle 样式。我们没有理由不绑定 Foreground 属性,而绑定 TextContent 属性:
但是,由于 Age 是 Int32 类型,而 Foreground 是 Brush 类型,所以需要从 Int32 映射到 Brush它应用到从 Age 绑定到 Foreground 的数据。这就是转换程序 的工作。转换程序是 IDataTransformer 接口的实现(与摧毁恶势力的诡计无关)。根据需要,转换程序可以用于将数据从源(如 Person 对象的 Age)转换到目标(如 ForegroundBrush),或者相反(尽管反向转换只有在控件中的数据可以更改的情况下才有必要,如 TextBox 及其 TextContent 属性)。要在 Int32Age 和 BrushForeground 之间映射,我们需要实现自定义 IDataTransformer 接口的 Transform 方法,如下所示:
public class AgeTransformer : IDataTransformer { public object InverseTransform(object obj, ...) { // Not mapping back from Brush to Int32 return obj; } public object Transform(object obj, DependencyProperty dp, ...) { int age = (int)obj; if( dp.Name == "Foreground" ) { // Map from Int32 to Brush if( age > 18 ) { return System.Windows.Media.Brushes.Red; } else { return System.Windows.Media.Brushes.Green; } } return obj; } }
在实现 Transform 方法的过程中,请注意要进行转换的对象作为对象的第一个参数出现。转换目标的属性作为 DependencyProperty 出现,它是一个包含很多属性的描述,但我们所使用的一个属性是 Name。这允许单个 Transformer 类在多个属性之间进行转换。
在我们获得转换程序后,我们要注意下面两个步骤:
第一步就是在 XAML 中定义名为 TransformerSource 的元素。TransformerSource 在我们将要在 XAML 中使用的用于指代自定义的转换程序类的名称和类本身的名称之间进行映射。TypeName 的格式为:
TypeName="Namespace.ClassName[,AssemblyName]"
根据上下文,AssemblyName 有时是可选的,但是想尽一切办法用于 WinHEC 版本中的各种结构。
第二步是将 TransformerSource 用于构建绑定样式中。一旦我们的自定义年龄转换程序准备就绪后,图 3 就会显示结果。
图 3. John 的年龄呈绿色,因为他小于 18 岁
而且,随着数据的更新,样式会重新应用,针对每次更改都会调用转换程序,如图 4 所示。
图 4. John 的年龄呈红色,因为他是 18 岁或大于 18 岁
Chris Sells 在向现有的数据绑定内容添加样式选择器后,对他以前的纸牌应用程序实现了进一步的构建。
下载 lhsol6.msi 示例文件。
请回忆一下本系列的上一篇文章中,我们有一个 Person 对象集合,该集合与一个 ListBox 控件和几个 TextBox 控件进行了绑定。我们使用该视图执行了筛选和排序,并且使用数据转换器绑定到了某个项样式中的 Foreground 颜色,如图 1 所示。
图 1. 操作中的 Avalon 数据绑定
比较重要的 XAML 内容,如下列代码段所示:
Persons ...
样式选择器
样式与转换器联合在一起形成了一对功能非常强大的组合,这种组合使您能够生成 UI 元素并根据数据来动态设置这些元素的属性。但是,在 XAML 中没有一个“if”语句使您能够根据数据来决定要显示哪些 UI 元素。例如,如果您想要隐藏年龄,并在年龄达到某个特定值时显示一个字符串,您执行此操作的最佳方式就是在 UI 元素上设置 Visibility 属性,如下所示:
在这种情况下,我们要绑定Age 属性两次 - 一次用于第一个 Text 元素,一次用于第二个 Text 元素。如果年龄不是太大的话(例如,在 30 岁以下),YoungVisibleTransformer 会返回真,OldVisibleTransformer 会返回假,因此将显示第一个 Text 属性,而不会显示第二个 Text 属性。如果某人的年龄为 30 岁或者超过了 30 岁,YoungVisibleTransformer 则会返回假,OldVisibleTransformer 返回真,因此会隐藏第一个 Text 元素,而显示第二个 Text 元素。即使对于如此简单的内容,也很难跟踪和要求两个类,所以您肯定希望我给您演示一种更简单的方式。好啊……,样式选择器来啦。
样式选择器 是一个代码段,它会根据数据本身来决定向该数据应用什么样式。例如,为了解决有关年龄显示/隐藏的问题,假设我们改用两个样式:
在这里,样式之间唯一的区别就是几个 Text 元素,但是您可以根据要显示的内容,设想任意数量的、彼此之间完全不同的样式。
自定义样式选择器会实现为一个类,该类从StyleSelector 基类派生并且它会覆盖一个名为 SelectStyle 的方法:
public class PersonStyleSelector : StyleSelector { Style youngStyle; Style oldStyle; public PersonStyleSelector(Style youngStyle, Style oldStyle) { this.youngStyle = youngStyle; this.oldStyle = oldStyle; } public override Style SelectStyle(object item, FrameworkElement container) { Person person = (Person)item; if( person.Age < 30 ) return this.youngStyle; return this.oldStyle; } }
回想一下,之前我们是使用ListBox 的 ItemStyle 属性来应用 PersonStyle 的:
要加入样式选择器,您需要改为设置ItemStyleSelector。但是,因为我原来需要创建 PersonStyleSelector 的实例,并将样式传递到构建函数,所以现在我要在该窗口的构建函数中设置列表框的 ItemStyleSelector 属性:
void Window1_Loaded(object sender, EventArgs e) { Style personStyle = (Style)this.Resources["PersonStyle"]; Style oldPersonStyle = (Style)this.Resources["OldPersonStyle"]; // Assumes an ID attribute set on theXAML element: // personsListBox.ItemStyleSelector = new PersonStyleSelector(personStyle, oldPersonStyle); ... }
加入样式选择器之后,每项都会在运行时根据数据获取其样式,如图 2 所示。
图 2. 操作中的自定义样式选择器
要注意的一点是,样式选择器是在第一次显示数据,然后刷新视图时立即使用的,而不是在数据更改时使用的。如果我们的示例 UI 要使 Age 字段成为只读字段的话,这将是最高效的方式。然而,将来版本的“Avalon”(Longhorn 中表示子系统的代号)应该会包括一个类似样式选择器的功能,并且该功能与实际的数据更改相关联。
其他数据源
当您确定数据如何显示在 Avalon 中时,样式与数据绑定的组合为您提供了各种形式的灵活性。与之相似,涉及到数据源时,Avalon 也具有相同程度的灵活性。例如,我已在主窗口类的 Loaded 事件处理程序中创建了 Person 对象集合。然而,如果我愿意,还可以通过 XAML 中的声明性操作创建该列表:
... ...
ObjectDataSource 元素与 TransformerSource 元素相似,它们都会采用一个 TypeName 来创建 CLR 类型的实例。然后,ObjectDataSource 的 def:Name 元素会用作 GridPanel 元素 DataContext 属性的值。这样就会创建 PersonsSource 的一个实例,我已经将其实现为 ArrayListDataCollection 类的扩展,该类会在创建时自行填充:
public class PersonsSource : ArrayListDataCollection { public PersonsSource() { this.Add(new Person("John", 10)); this.Add(new Person("Tom", 8)); } }
上述操作非常容易做到,但至少在 Longhorn 的 WinHEC Build 中,您无法将 DataContext 的范围设置得与 ObjectDataSource 所声明的范围相同。这种情况就意味着要将 DataContext 移动到 GridPanel,而不是在该窗口本身中进行设置。
与所有 XAML 元素一样,ObjectDataSource 只是一个 CLR 类。具体地说,它是一个实现 IDataSource 接口的类。除了 CLR 对象图之外,Avalon 数据绑定还支持绑定到 SQL 数据、WinFS 数据以及 XML 数据,所有这些数据都具有相似的 IDataSource 实现(分别为 SqlDataSource、WinFSDataSource 和 XmlDataSource)。
这对于纸牌程序会有哪些影响呢?
那么,您还记得“Another Step Down the Longhorn Road”这篇文章吧,我在那篇文章里构建了一个 Longhorn 版本的纸牌程序。您可能会问,所有这些数据绑定内容对于纸牌程序会产生什么影响呢?在那篇文章中,在我努力将 Avalon 数据绑定到服务之前,我曾经通过枚举基础堆栈数据结构并将这些项添加到一个列表框控件中,来填充 PileOfCards 控件:
public partial class PileOfCards : Canvas { Stack stack; public Stack StackOfCards { get { return this.stack; } set { this.stack = value; ShowStack(); } } void ShowStack() { cardList.Items.Clear(); foreach( Card card in this.stack ) { string cardValue = string.Format("{0}{1}", card.Value, card.Suit); if( !card.Flipped ) cardValue = "[" + cardValue + "]"; // Fills XAML ListBox declared like so: //cardList.Items.Add(cardValue); } } }
这样就产生了一个与图 3 所示内容相似的只读显示。
图 3. 数据绑定之前的 Longhorn 纸牌程序
这种显示不但非常难看,而且如果我一直采用这种方案的话,则每当纸牌程序基础数据发生变化时我都必须手动更新该列表框控件。因为有了本文和上一篇文章所演示的数据绑定技巧,所以我能够获得您在图 4 中所看到的内容,所有这些均无需在控件和基础数据之间手动地来回移动数据。
图 4. 使用 Avalon 数据绑定的 Longhorn 纸牌程序
要实现 PileOfCards.Stack 属性,只需设置控件的 DataContext 就可以了:
public partial class PileOfCards : Canvas { public Stack StackOfCards { get { return this.DataContext as Stack; } set { this.DataContext = value; } } ... }
为了反映基础数据中的变化,我将纸牌程序引擎更新为使用 IPropertyChange 和 ICollectionChange:
public class Card : IPropertyChange { public event PropertyChangedEventHandler PropertyChanged; void RaisePropertyChanged(string propertyName) { if( this.PropertyChanged != null ) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } ... bool flipped; public bool Flipped { get { return this.flipped; } set { this.flipped = value; RaisePropertyChanged("Flipped"); } } } public abstract class Stack : IEnumerable, ICollectionChange { public event CollectionChangeEventHandler CollectionChanged; void RaiseCollectionChanged(CollectionChangeAction action, object obj) { if( this.CollectionChanged != null ) CollectionChanged(this, new CollectionChangeEventArgs(action, obj)); } internal Card[] GrabCards(int i) { ... RaiseCollectionChanged(CollectionChangeAction.Refresh, null); ... } ... }
列表中这些纸牌的样式是通过使用样式、数据绑定和转换来实现的:
我希望您自己来查看这段源代码来了解所有的繁琐细节,但是有几点内容我要指出来。例如,您要注意 ListBox 没有 ItemsStyle 属性。相反,使用设置为 *typeof(sol:Card) 的 Style 元素的 def:Name 属性,将样式匹配为列表中的纸牌。前缀“sol”是 XML 命名空间前缀,设置该前缀是为了引用在其中定义了纸牌类型的 DLL(正如我在 Another Step Down the Longhorn Road 一文中讨论的那样)。在 WinHEC Longhorn Build 中,只有当类型是在一个单独 DLL 中定义的情况下,此类基于类型的样式映射才起作用,但是与手动映射列表框控件中的样式相比,我更喜欢使用这种方法,尤其是当您的列表可能是异类列表,并且同一个列表中的不同类型可能具有不同的样式时(当然还有一种方法可以在运行时将样式映射到项)。
另一个要注意的问题是,根据纸牌朝上还是朝下,来决定是否使用 Visibility 转换器将纸牌括在方括号内。就是说,[A?] 表示朝下,而 A? 表示朝上。
最终,最让我感到骄傲的转换器就是用于这套纸牌的转换器。请注意,我将 Text 元素的 FontFamily 属性硬编码为 Symbol。这就是纸牌图形的出处。SuitTransformer 的实现根据用于进行设置的属性,将 Suit 枚举值映射到 Symbol 字体的相应颜色和字符:
public class SuitTransformer : IDataTransformer { ... public object Transform(object obj, DependencyProperty dp, CultureInfo culture) { // Heart, Diamond, Spade and Club in Symbol font string textContents = "©¨ª§"; Brush[] foregrounds = { Brushes.Red, Brushes.Red, Brushes.Black, Brushes.Black }; switch( dp.Name ) { case "Foreground": return foregrounds[(int)obj]; break; case "TextContent": return textContents[(int)obj].ToString(); break; default: return obj; break; } } }
我还需要做另一个操作,就是添加筛选器,以便只有最上面的三张纸牌显示在弃牌堆中,只有最上面的一张纸牌显示在发牌堆中,等等。尽管如此,您也会感到惊奇,Avalon 中的数据绑定居然能让我完成那么多的任务(另外,当我想到如何获取图形时,Peter Stern 还提供了一些图形供我使用)。
一些问题
但是,并不能说 Avalon 中的一切都是完美的。我的意思是,毕竟在正式发布 Longhorn 之前,还要经过几年的历练。除了我前面一直在说的问题之外,下面是我遇到的另外几个问题:
•
尽管使用数据绑定,但当我删除一个纸牌序列以执行拖放操作时,还是必须手动刷新该控件才能显示在拖放过程中纸牌已经从牌堆消失。我希望在将来的内部版本中能够简化这个过程。
•
即使为了启动拖放过程,我还是需要执行一些点击测试,才能知道单击了哪张纸牌。要想找出如何完成上述操作非常困难,甚至比使其运行更加困难(尤其是因为在 WinHEC Build 中无法运行 ItemUIGenerator.IndexFromUI)。该操作一定要再简单一些。
•
纸牌程序引擎在其 Stack 数据结构中实现 IEnumerable
•
虽然样式选择器好像非常适用于翻转纸牌和取消翻转纸牌,但是因为它还不够动态,所以我也无法使用。例如,Flipped 属性发生变化时,不能调用 SelectStyle 方法来切换样式。我听说这是设计使然,希望在将来内部版本的 Longhorn 中能够有一些更加动态的方式,可以用来基于数据更改来切换样式,但是等待的日子总是很难熬啊!
我们所处的位置
您应该知道,以上并不是 Avalon 数据绑定的所有用途。将来的内部版本中不但会有其他功能和改进,而且还会有一些诸如在相同数据上使用多个视图、更少数据视图或者主从关系等功能,这些我以前没有提到过。但当我向应用程序添加数据绑定的时候,我还是认识到了数据绑定的 Avalon 模型与以前在 Windows 表单或 ASP.NET 中所用到数据绑定模型的不同之处。
在 Windows 表单中,数据绑定就是设置属性值或者填充属性集合(例如 ListBox 控件上的项集合)。在 ASP.NET 中,数据绑定则是生成 HTML,并将其发送回客户端(例如 HTML 选择控件中的可选元素)。
因为 XAML 语法的缘故,Avalon 好像与 ASP.NET 有些相似,但是我们并不生成更多的 XMAL。Avalon 好像与 Windows 表单模型也有些相似,但是我们并不只是向 Item 集合中添加字符串。相反,Avalon 数据绑定一些新的内容。虽然 XAML 本身是静态的,并且经常被编译到应用程序中,但是我们会在运行时在可视树中生成 UI 元素。因此,为了使连接服务器的智能客户端能够生成动态内容,它们不一定要收集 XAML,而会将数据绑定到在静态 XAML 中定义的 Avalon 样式,始终使用样式、样式选择器和转换器来根据数据决定生成哪些元素。