发布日期: 09/19/2004 | 更新日期: 09/19/2004
Chris Sells
Microsoft Corporation
摘要: Chris Sells 探究视图如何维持当前项目以及视图如何支持筛选和排序。然后,他研究绑定到一种类型的数据并映射到另一种类型的数据的转换,或者位于不同范围的值中的转换。
本页内容
我们所处的位置
请回忆一下上一篇文章中,我们希望能够直接将对象和对象的集合绑定到 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 对象的样式:
<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>
注意,这段代码绑定到 Text 元素的 TextContent 属性,而 Text 元素则构成了用于从列表框中呈现项目的 PersonStyle 样式。我们没有理由不绑定 Foreground 属性,而绑定 TextContent 属性:
<!-- NOTE: Need more to bind the Foreground to the Age -->
<Text TextContent="*Bind(Path=Age)" Foreground="*Bind(Path=Age)" />
但是,由于 Age 是 Int32 类型,而 Foreground 是
Brush 类型,所以需要从 Int32 映射到
Brush它应用到从 Age 绑定到 Foreground 的数据。这就是转换程序 的工作。转换程序是 IDataTransformer 接口的实现(与摧毁恶势力的诡计无关)。根据需要,转换程序可以用于将数据从源(如 Person 对象的 Age)转换到目标(如 Foreground
Brush),或者相反(尽管反向转换只有在控件中的数据可以更改的情况下才有必要,如 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 类在多个属性之间进行转换。
在我们获得转换程序后,我们要注意下面两个步骤:
<Window.Resources>
<!-- 1. Define a TransformerSource -->
<TransformerSource def:Name="AgeTransformer" TypeName="PersonBinding.AgeTransformer" />
<Style def:Name="PersonStyle">
<Style.VisualTree>
<FlowPanel>
<Text TextContent="*Bind(Path=Name)" />
<Text TextContent=":" />
<!-- 2. Add Transformer to Bind -->
<Text TextContent="*Bind(Path=Age)" Foreground="*Bind(Path=Age;Transformer={AgeTransformer})" />
<Text TextContent=" years old" />
</FlowPanel>
</Style.VisualTree>
</Style>
</Window.Resources>
第一步就是在 XAML 中定义名为 TransformerSource 的元素。TransformerSource 在我们将要在 XAML 中使用的用于指代自定义的转换程序类的名称和类本身的名称之间进行映射。TypeName 的格式为:
TypeName="Namespace.ClassName[,AssemblyName]"
根据上下文,AssemblyName 有时是可选的,但是想尽一切办法用于 WinHEC 版本中的各种结构。
第二步是将 TransformerSource 用于构建绑定样式中。一旦我们的自定义年龄转换程序准备就绪后,图 3 就会显示结果。
图 3. John 的年龄呈绿色,因为他小于 18 岁
而且,随着数据的更新,样式会重新应用,针对每次更改都会调用转换程序,如图 4 所示。
图 4. John 的年龄呈红色,因为他是 18 岁或大于 18 岁
我们所处的位置
在[url=http://www.msdn.microsoft.com/longhorn/understanding/columns/default.aspx?pull=/library/en-us/dnfoghorn/html/foghorn06252004.asp]本系列的第一部分中,我们曾经研究过有关将对象和对象集合绑定到项目和列表控件的数据的基础知识,包括包含数据的对象和集合(IPropertyChange 和 ICollectionChange)的要求。在第二部分中,我们探究了视图如何维持当前项目以及视图如何支持筛选和排序。我们还探究了绑定到一种类型的数据,然后将其映射到另一种类型的数据或者位于不同范围的值中的转换。所有这些功能使 Avalon 数据绑定极其强大和灵活。在本系列的最后部分中,我们将继续探究增强 Avalon 能力和灵活性的更多功能,包括在运行时选择不同的样式以应用到列表中的每个项目。最后,我们将看到所有这些数据绑定功能如何应用到手头的问题 - 实现 Solitaire。
致谢
我要再次感谢 Namita Gupta,她是 Avalon 数据绑定的程序经理。尽管从事有关这项技术方面的工作已有三年时间,她仍然对我所有比较浅薄的问题给予耐心地答复,并一直倾听我的抱怨。
同时,像往常一样,我要感谢 Mike Weinhardt 所制作的非常酷的图片(本例中的图 2)。他已经是这方面的高手了。
参考资料
•
Crazy About Avalon Data Binding,作者 Chris Sells
•
Avalon Styles Overview,Longhorn SDK
•
Avalon Data Binding,Longhorn SDK
•
Chris Sells 是 MSDN Online 的内容战略家,当前专注于研究 Longhorn(Microsoft 的下一个操作系统)。他已经编写了很多书籍,包括 Mastering Visual Studio .NET 和 Windows Forms Programming in C#。在业余时间,Chris 主持各种会议,指导 Genghis 可用源项目,和 Rotor 一起休闲娱乐,并且还经常在 blogsphere 中发表一些文章。有关 Chris 及其各种项目的详细信息,请访问 http://www.sellsbrothers.com。