目前为止,你已经看到一些示例将控件绑定到一个单独的对象。然而,更复杂的使用是绑定到一个对象列表。例如,想象一下,我们的对象数据源可以创建一个新类型表示Person对象的列表,正如示例4-19:
示例4-19
我们可以挂起这个新的数据源列表,按照同样的方式绑定到它,就像绑定到一个单独的对象数据源上,如示例4-20。
示例4-20
在示例4-20中,我们创建了一个People集合的示例而且通过三个Person对象导入它。然而,运行它将会如图4-6。
4.3.1当前项
尽管文本框属性每次仅能被绑定到一个单独的对象上,在可能的被绑定到的对象列表中,绑定引擎提供了一个名为当前项的概念,正如图4-6所解释的。
缺省地,列表的第一项作为当前项的开始。由于我们列表示例的第一项与我们之前绑定的单独对象一样,所以看起来和图4-11显示的一样——Birthday按钮除外。
图4-11
4.3.1.1获取当前项
回想当前Birthday按钮的click事件句柄(示例4-21)。
示例4-21
我们的Birthday按钮应该总是产生向当前人士祝贺生日的效果,但是到目前为止,当前人士却总是一样的,因此我们只能简化事情为直接到达单独的Person对象。既然我们已经得到了对象的列表,这个机制就不再使用了(除非你认为一个包含单词“InvalidCastException”消息框是可以接受的方式)。进一步而言,转换到People,我们的集合类,不会告诉我们那一个Person对象会在当前UI中显示,因为它不知道这些事情(也不需要知道)。由于这一点,我们将要必须建立“经纪人”在数据绑定的控件和集合项上,这个“经纪人”在这里被称为视图。
视图的工作是在数据之上提供服务,包括排序,过滤,以及此刻对于我们的意图来说最重要的:控制当前项。视图是详细数据的接口实现,在我们这种情形,就是ICollectionView接口。我们可以通过BindingOperations类的静态GetDefaultView方法访问这个数据上的视图,正如示例4-22所示:
示例4-22
为了取回联合了Family集合的视图,示例4-22对BindingOperations的GetDefaultView方法进行了一次调用,提供了一个ICollectionView接口的实现。基于此,我们可以得到当前项,将它从集合中的一项转换为我们需要的对象(CurrentItem属性返回一个object对象),以及用它来显示。
4.3.1.2在数据项中导航
出了获取当前项外,我们也能改变当前项的位置,通过ICollectionView接口的MoveCurrentToXX方法,正如示例4-23所示。
示例4-23
ICollectionView接口的MoveCurrentToPrevious方法和MoveCurrentToNext方法,通过在集合中向后和向前的动作改变当前的选中项。如果我们沿着一个方向移动到列表的尽头或另一个尽头,IsCurrentBeforeFirst或IsCurrentAfterLast属性将会告诉我们这一点。MoveCurrentToFirst和MoveCurrentToLast方法帮助我们复原在到达列表的尽头之后,对于在途4-12中实现Back和Forward按钮,这将是很有用的。同样适用于First和Last两个按钮(这将是你的一个机会,将学到的运用上去)。
图4-12显示了从集合中第一个Person元素开始,向前移动的效果,包括基于Person对象的Age属性导致的颜色改变(这仍然以同样的方式工作)。
图4-12
4.3.2数据列表目标
当然,目前为止,我们仅能做的是把用户列表数据推出来,而没有为这些数据提供一个控件可以准确的一次性显示多条数据,正如示例4-24中的ListBox控件。
示例4-24
在示例4-24中,ListBox的ItemSource属性没有绑定到路径,等于是说:绑定到当前整个对象。注意到,这里也没有源,因此绑定会从找到的第一个非空的数据上下文开始工作。在这种情形中,第一个非空的数据上下文来自Grid,就是那个在name和age的文本框中共享的上下文。我们还设置了IsSynchronizedWithCurrentItem属性为true,以确保listbox中的选中项也能发生改变——这会在视图中更新当前项;反之亦然。
图4-13
正如你可能看到的,图4-13中的每件事物都很完美。所发生的是,当你绑定一个完整对象时,数据绑定尽其所能显示每一个Person对象。无需特殊的指令,它会使用一个类型转换器来得到一个字符串表示。对于name和age,都是内嵌类型,具有内嵌转换,这将工作良好;但是也有不能很好工作的时候,对于一个不具备可视化生成的自定义类型,正如Person类型这种情形。
4.3.3数据模板
正确解决这个问题的做法是使用数据模板。数据模板是一棵元素树,可以在特定的上下文扩展。例如,对于每一个Person对象,我们希望能够像以下方式将name和age连接在一起:
Tom(age: 9)
我们可以把它想象成一个合乎逻辑的模板,如下:
Name(age: Age)
为了在listbox中为数据项定义模板,我们创建了一个DataElement元素,正如示例4-25。
示例4-25
在这种情形中, ListBox控件有一个ItemTemplate属性,它接受一个DataTemplate对象示例。DataTemplate允许我们详细指出一个单独的子元素,用于绑定重复显示在ListBox控件的每一个数据项。在我们的例子中,使用了StackPanel将四个TextBlock控件放在一行中:2个绑定到每个Person对象的属性,两个是常量文本。注意到,我们使用AgeToForegroundConverter已经将Foreground绑定到Age属性,为了Age属性显示为黑色或红色,为了列表框和age文本框是一致的。
通过使用数据模板,我们经历了从图4-13到图4-14。
图4-14
注意到,列表框显示了集合中所有的条目,而且保持了视图同步于当前条目,当选择向前或向后的按钮按下时(实际上,你并不会从图4-14的部分截图真正“注意到”,但是相信我,确实是发生了)。此外,当Person对象的数据改变的时候,列表框以及文本框会保持同步,还包括Age的颜色。
4.3.1类型化数据模板
在示例4-25中,我们显示地为ListBox列表设置了数据模板。然而,如果一个Person对象显示在一个按钮或是其它什么元素中,我们最好分别详细指出那些Person对象的数据模板。另一方面,如果你想要Person对象有一个特殊的模板而不论其显示在哪里,你可以通过类型化的数据模板来实现。
示例4-26
在示例4-26中,我们将数据模板的定义提升到资源模块,并且使用标签的DataType属性标志这个数据模板是类型化的。现在,除非另外通知,每当WPF看到Person对象的一个实例,就会应用相应的数据模板。这是一条便利之路,保证数据以一致的方式显示,遍及于你的应用程序,而不用担心显示的位置。
4.3.4列表的改变
迄今,我们已经得到一个对象的列表,我们可以适当的进行编辑,以及在其中建立导航,甚至轻而易举地高亮显示某些数据,以及提供了一个自动搜索,表现那些没有装载的来自厂商的数据。考虑到我们已经到达的程度,你可能怀疑提供一个Add按钮是一件轻而易举的事情,正如示例4-27所示。
示例4-27
这个实现的问题在于,尽管视图可以判断出新条目的存在当你移动到这里的时候,而列表框本身却并不知道新增加的集合中的条目,正如图4-15。
图4-15
为了与图4-15显示的应用程序状态交互,我运行了这个程序,点击了Add按钮并使用Forward按钮导航到图中所示。然而,即使新人显示在文本框中,列表框仍然不知道添加了什么事物。同样地,如果有对象被删除,它也不会知道。就像数据绑定需要事先INotifyPropertyChanged接口,使用数据绑定的列表需要实现INotifyPropertyChanged这个接口,正如示例4-28。
示例4-28
INotifyCollectionChanged接口用于通知数据绑定控件,有条目在绑定列表中添加或删除。尽管在你的自定义类型中实现INotifyPropertyChanged,从而支持两种方式的数据绑定在你的类型化属性上——这很普通;不普通的是实现你自己的集合类,这些类给你很少的机会实现INotifyCollectionChanged接口。取代之,你更加更能依赖于集合类的一项在.NET 框架类库中,用来实现INotifyCollectionChanged。这样的类数量很少,而且不幸的是,我们使用的保持着Person对象的集合类,并不在其中。当你受欢迎的度过你的夜晚和周末实现了INotifyPropertyChanged,WPF提供了ObservableCollection<T>类,用于我们那些紧迫的职责,如示例4-29所示。
示例4-29
既然ObservableCollection<T>派生于Collection<T>,而且实现了INotifyCollectionChanged接口,我们可以使用它代替List<T>作为我们的Person集合,正如示例4-30。
示例4-30
现在,当一个条目添加到或删除自Person集合,这些变化将要在数据绑定列表中反映出来,正如图4-6所示。
图4-16
4.3.5排序
一旦我们适当地使数据目标每次显示多于一个事物,一个年轻人的爱好变得更多,当然,是喜欢的事物,正如对数据视图排序或者过滤。回忆视图经常位于数据绑定目标和数据源之间。这意味着可以越过我们不要显示的数据(被称为过滤,而且可以被直接覆盖),而且可以改变数据显示的顺序,又名排序。最简单的排序方法是通过操作视图的Sort属性,正如示例4-31所示。
示例4-31
这里我们通过检测SortDescriptionCollection暴露在外的ICollectionView.Sort属性,将排序视图和未排序视图拴在一起。如果没有排序方式的描述,我们首先对Name属性按上升方式排序,然后对Age属性按下降方式排序。如果有排序方式的描述,我们将其清除,重新排序——无论之前是如何排序的。虽然排序描述在适当的位置,任意新添加到集合中的对象将被添加到它们已经排好序的适当位置,正如4-17所示。
一个SortDescription对象集合应该覆盖大多数的情形,但是如果你需要更多一点的控件,你可以提供自定义排序对象的视图,通过实现IComparer接口,正如示例4-32。
图4-17
示例4-32
在设置了自定义排序的情况,我们必须做一个假设——详细明确地实现了ICollectionView,这里使用的是ListCollectionView,是WPF包装在IList的实现(由ObserverableCollection提供),来提供视图的功能性。此外还有其它没有提供自定义排序的ICollectionView接口实现,因此你要在想*使用这段代码前先测试一下。
希望你在使用前也测试一下其它代码,但是指出这些事情并没有什么危害。
尽管我肯定,当我们使用WPF1.0时,这将变得更好。从现在开始,视图实现了联合详细数据特征,正如在ListCollectionView和IList间进行匹配并没有文本化(至少现在我这么说)。这看起来有点有趣,CustomSort是视图实现类的一部分,并不是ICollectionView接口的一部分,因此让我们为之祈祷:Microsoft发布新的WPF版本改变这一点。
4.3.6过滤
正因为所有的对象按顺序显示使你快乐,这并不意味着你想要显示所有的对象。对于这些没用的出现在数据中的对象,却不属于这个视图,我们需要提供这个实现了CollectionFilterCallback委托*的视图,需要一个单独的对象作为参数并返回一个Boolean值表明这个对象是否应该被显示,正如示例4-33。
排序使用一个单方法的接口实现,是由于历史原因;而过滤使用一个委托,是因为在C#2.0中另外使用匿名委托机制,这是一个很流行的机制。
示例4-33
正如排序,通过使用一个恰当的过滤器,新条目被适当的过滤掉了,正如图4-18所示。
图4-18
图4-18中最上面的窗体显示了没有过滤器,中间的窗体显示了过滤了初始的列表,底部的窗体显示了添加一个成年人,过滤器仍然在恰当的位置。