ItemsControl: 'C' is for Collection
(该系列的第二章是关于ItemsControl的一些有趣的东西,但是跟了解ItemsControl不是很相关,故此略过)
如果没有一个项的集合,那么ItemsControl将会一无所有。在这篇文章里,我们将会调查ItemsControl的Items属性,关注以下话题:
· The Items Collection
· Item Collection Modes
· Observable Collections
· CollectionView: “The Great Equalizer”
· Performance Considerations
· Items and the Element Trees
ItemsCollection
Items属性提供了对一个集合中对象的访问方法。这些对象构成了ItemsControl的逻辑内容。该属性的类型是ItemsCollection。ItemsCollection中的对象是Object类型。所以表面上看来,任何CLR对象都可以被加入到该集合中。
在更详细的讨论ItemsCollection之前,还要关注一下Items属性。
1) 该属性是一个只读CLR属性。
这意味着该集合只能被控件本身实例化。实际上,ItemsCollection类甚至没有一个共有的构造函数。
2) Items属性并不是一个依赖属性。
这意味着你不能在Items属性上应用绑定。然而,你绝对可以将一个集合的对象绑定到ItemsControl上,这点我们稍后再讲。现在让我们先看看非绑定情景下对ItemsControl的使用。
1. ItemsCollection模式:直接模式和ItemsSource
虽然Items属性是只读的,但是,它所提供的集合不是只读的。实际上,我们已经看到,项可以被直接加入到ItemsControl中:
<ListBox>
<sys:String>Item 1</sys:String>
<sys:String>Item 2</sys:String>
<sys:String>Item 3</sys:String>
</ListBox>
上面就是对ItemsCollection使用“直接模式”。直接模式非常简单,在直接模式下,ItemsCollection类跟其它.NET集合类一样。你可以访问集合的任何成员: Add(), Insert(), Remove(), RemoveAt(), IndexOf(),Items[index]。
ItemsControl另一个模式是ItemsSource模式。在此模式下,ItemsCollection中的项对应到另外一个对象集合。这个对象集合通过ItemsControl的另外一个属性指定,这个属性是ItemsSource。
下面的标记显示了如何使用ItemsSource
<ListView ItemsSource="{BindingPath=Characters}">
<ListView.View>
<GridView>
<GridViewColumn Width="100"
DisplayMemberBinding="{Binding Last}"
Header="Last Name" />
<GridViewColumn Width="100"
DisplayMemberBinding="{Binding First}"
Header="First Name" />
<GridViewColumn Width="60"
DisplayMemberBinding="{Binding Gender}"
Header="Gender" />
</GridView>
</ListView.View>
</ListView>
ItemsSource属性是一个类型为IEnumerable的依赖属性。这告诉我们两件事情:
1) 源集合可以是任何enumerable集合。
2) 可以在ItemsSource属性上应用数据绑定。
因为如此,所以可以使用该属性将ItemsControl绑定到一个集合。
备注:虽然你经常看到使用数据绑定来设置ItemsSource,但是没有理由不可以直接设置,如下所示:
<ListBox ItemsSource="{StaticResourceCharacters}" />
2. 两种模式相互排斥
需要注意的是直接模式和ItemsSource模式相互排斥。一个ItemsCollection可以应用直接模式或者ItemsSource模式下,但是不能同时应用两种模式。
一旦Items属性被显式加入了对象,那么ItemsCollection就被设置为直接模式。此后如果设置ItemsSource属性将会引发异常。
同样,一旦ItemsSource属性被设置了,ItemsCollection就被设置为ItemsSource模式。此后如果项直接修改Items集合,将会引发异常。
在运行时更改模式有两种方法。
1) 在直接模式下,设置ItemsSource前通过使用Clear方法清除Items集合。
2) 在修改Items属性前,将ItemsSource属性设置为null。
Observable Collections 支持动态更新
在直接模式下,在运行时对ItemsCollection的修改会直接引起UI的更新。
但是在ItemsSource模式下的动态集合会不会引起UI更新呢?ItemsCollection如何知道源集合的变化呢?
答案是ItemsCollection不能知道源集合的变化。除非这些集合提供了变化通知。提供变化通知的方法是这些集合需要实现INotifyCollectionChanged接口。所有实现该接口的集合都被称为可观察集合。
observable collection的任何变化都被直接反映到ItemsControl的ItemsCollection中,并最终引发UI变化。
INotifyCollectionChanged接口并不复杂,但是如果开发人员每次在绑定所用集合时都需要实现该接口,会非常不方便。幸运的是,.NET框架提供了一个叫做ObservableCollection<T>的泛型模板。通过创建此类型的实例,你自动获得集合变化通知的能力。
典型的,你可以看到一个类可以从ObservableCollection<T>类型派生,如下:
public class StringCollection : ObservableCollection<string>
{
}
任何StringCollection的实例都是可观察的,而且能够ItemsSource属性联合起来工作。
那么如果集合不是可观察的呢?还可以使用在ItemsSource属性上吗?
当然,前面讲到,任何集合都可以赋给ItemsSource属性。唯一的不同是,如果源集合更新了,ItemsCollection将不会自动更新。当ItemsSource属性被赋值的时候,源集合将被遍历,所有的项将会被加入到ItemsCollection中。之后,如果你希望ItemsCollection被更新,那么你需要显式调用ItemsCollection.Refresh方法。
3. CollectionView:“伟大的平衡者”
如果你经常使用ItemsControl,那么你知道排序,分组和过滤这样功能是通过CollectionView支持的。这个类还支持“当前项”。CollectionView维护了一个当前项的指针。该指针可以通过CollectionView的方法来访问及移动。
每个WPF中的enumerable collection都有一个默认视图,同时还可能有其他多个视图,每个视图都有自己的排序分组和过滤参数。建立集合视图的通用方法是通过CollectionViewSource类。如下所示:
<CollectionViewSource x:Key="characterView"
Source="{StaticResource Characters}">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="First" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<dat:PropertyGroupDescription PropertyName="Last" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
这个CollectionViewSource对象可以作为ItemsControl的ItemsSource属性被指定。此后,其所关联的视图将会被ItemsControl使用。
“但是爸爸,我不想使用CollectionView”
“我没有问你想要什么,只要你在我的屋檐下,你就得使用CollectionView!”
在WPF中,有时候并不关心你想要什么,而是关心框架需要什么。CollectionView就是个经典的例子。框架需要用一种统一的方式来对待所有类型的集合,使得这些集合可以绑定到ItemsControl。
不幸的是,所有的集合并不是生来就平等。例如,IList支持项索引。而IEnumerable需要遍历很多项来得到指定索引的项。此外,有些集合类实现了INotifyCollectionChanged,而Collection<T>没有实现这样的功能。
框架架构师想要对尽可能多的集合类型支持绑定。但是想象如果我们需要对不同类型的集合的不同之处负责,那么ItemsControl中的代码将会有多么难看,如果是可观察的,那么……,否则如果是……,那么……等等。
为了处理这种挑战,WPF引入了CollectionView类作为不同集合的平衡器。是CollectionView类在内部观察集合所支持的接口,并决定如何用最好的方法处理集合。并通过视图公开一系列定义良好的属性,方法和事件。本质上,它允许ItemsControl用同一种方式对待所有集合。
所以无论你关心不关心当前项,排序,分组,过滤,变化通知等等。ItemsControl始终使用CollectionView在内部维护一个项集合。实际上,ItemCollection就是一个项集合。
虽然是真的,但是最后一句话有点误导。因为ItemColleciton只是一个内部CollectionView的包装器。内部的CollectionView的类型由源集合的类型和ItemCollection的模式决定。在直接模式,是InnerItemCollectionView(一个内部类)。在ItemsSource模式下,对于IEnumerable源是CollectionView,对于IList源是ListCollectionView;对于IBindingList或者IBindingListView是BindingListCollectionView。
4. 谁关心你是observable的?
CollectionView关心,我们知道,集合变化引发UI及时更新。这是因为CollectionView类在监听源集合的事件。
绑定集合的性能考虑
记住CollectionView是一个伟大的集合平衡者。对于绑定到一个集合,我们应该考虑一下它的性能问题。我们已经知道,不是所有的集合都支持同样的功能。CollectionView经常需要为源集合执行额外的工作来支持它的通用接口。
CollectionView提供的一个主要功能是为enumerable集合支持索引。也就是说CollectionView需要使用整数下标访问集合。这表示源集合需要支持this[int index]和Count,同时也要支持Contains方法和IndexOf方法。
如果源集合已经支持索引,你会得到更好的性能,这表示最好使用支持IList接口的集合。注意到ObservableCollection<T>实现了IList,所以这是一个不错的选择。
如果源集合不支持索引访问(ICollection,IEnumerable),CollectionView需要做很多的工作填补这些空白。为了支持Count属性,CollectionView可能需要遍历整个集合。其他方法,例如Contains,IndexOf等也是很昂贵的操作。因为这些支持这些操作的算法的性能取决于集合的大小。
所以为了获取高性能,需要源集合尽可能支持IList。
5. Items和元素树
ItemsCollection中的项构成了ItemsControl的逻辑孩子。它们是逻辑树的成员,对于HeaderedItemsControl,每个项关联的标头也是ItemsControl的逻辑孩子。
如果ItemsCollection中的项恰好是visual。那么这些项也是可视树的孩子。如果他们不是visual,这些项将会被模板展开后的visual所呈现。因为这些模板表示数据项,所以被称为DataTemplate。想了解DataTemplate,请继续收看‘D’is for DataTemplate。