目前为止,你已经看到一些示例将控件绑定到一个单独的对象。然而,更复杂的使用是绑定到一个对象列表。例如,想象一下,我们的对象数据源可以创建一个新类型表示Person对象的列表,正如示例4-19:
示例4-19
namespace PersonBinding {
// XAML doesn't (yet) have a syntax
// for generic class instantiation
class People : List<Person> {}
}
我们可以挂起这个新的数据源列表,按照同样的方式绑定到它,就像绑定到一个单独的对象数据源上,如示例4-20。
示例4-20
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window xmlns:local="local">
<Window.Resources>
<local:People x:Key="Family">
<local:Person Name="Tom" Age="9" />
<local:Person Name="John" Age="11" />
<local:Person Name="Melissa" Age="36" />
</local:People>
<local:AgeToForegroundConverter
x:Key="AgeToForegroundConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
<TextBlock >Name:</TextBlock>
<TextBox Text="{Binding Path=Name}" />
<TextBox
Text="{Binding Path=Age}"
Foreground="{Binding Path=Age, Converter=}" />
<Button >Birthday</Button>
</Grid>
</Window>
在示例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
void birthdayButton_Click(object sender, RoutedEventArgs e) {
Person person = (Person)this.FindResource("Tom"));
++person.Age;
MessageBox.Show();
}
}
我们的Birthday按钮应该总是产生向当前人士祝贺生日的效果,但是到目前为止,当前人士却总是一样的,因此我们只能简化事情为直接到达单独的Person对象。既然我们已经得到了对象的列表,这个机制就不再使用了(除非你认为一个包含单词“InvalidCastException”消息框是可以接受的方式)。进一步而言,转换到People,我们的集合类,不会告诉我们那一个Person对象会在当前UI中显示,因为它不知道这些事情(也不需要知道)。由于这一点,我们将要必须建立“经纪人”在数据绑定的控件和集合项上,这个“经纪人”在这里被称为视图。
视图的工作是在数据之上提供服务,包括排序,过滤,以及此刻对于我们的意图来说最重要的:控制当前项。视图是详细数据的接口实现,在我们这种情形,就是ICollectionView接口。我们可以通过BindingOperations类的静态GetDefaultView方法访问这个数据上的视图,正如示例4-22所示:
示例4-22
void birthdayButton_Click(object sender, RoutedEventArgs e) {
People people = (People)this.FindResource("Family");
ICollectionView view =
BindingOperations.GetDefaultView(people);
Person person = (Person)view.CurrentItem;
++person.Age;
MessageBox.Show();
}
}
为了取回联合了Family集合的视图,示例4-22对BindingOperations的GetDefaultView方法进行了一次调用,提供了一个ICollectionView接口的实现。基于此,我们可以得到当前项,将它从集合中的一项转换为我们需要的对象(CurrentItem属性返回一个object对象),以及用它来显示。
4.3.1.2在数据项中导航
出了获取当前项外,我们也能改变当前项的位置,通过ICollectionView接口的MoveCurrentToXX方法,正如示例4-23所示。
示例4-23
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void birthdayButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
Person person = (Person)view.CurrentItem;
++person.Age;
MessageBox.Show();
}
void backButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
view.MoveCurrentToPrevious( );
if( view.IsCurrentBeforeFirst ) {
view.MoveCurrentToFirst( );
}
}
void forwardButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
view.MoveCurrentToNext( );
if( view.IsCurrentAfterLast ) {
view.MoveCurrentToLast( );
}
}
}
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
<?Mapping XmlNamespace="local" ClrNamespace="PersonBinding" ?>
<Window xmlns:local="local">
<Window.Resources>
<local:People x:Key="Family"></local:People>
<local:AgeToForegroundConverter
x:Key="AgeToForegroundConverter" />
</Window.Resources>
<Grid DataContext="{StaticResource Family}">
<ListBox
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True" />
<TextBlock >Name:</TextBlock>
<TextBox Text="{Binding Path=Name}" />
</Window>
在示例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>
<StackPanel Orientation="Horizontal">
<TextBlock TextContent="{Binding Path=Name}" />
<TextBlock TextContent=" (age: " />
<TextBlock
TextContent="{Binding Path=Age}"
Foreground="
{Binding
Path=Age,
Converter={StaticResource AgeToForegroundConverter}}" />
<TextBlock TextContent=")" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
在这种情形中, 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
<local:AgeToForegroundConverter
x:Key="AgeToForegroundConverter" />
<local:People x:Key="Family"></local:People>
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel Orientation="Horizontal">
<TextBlock TextContent="{Binding Path=Name}" />
<TextBlock TextContent=" (age: " />
<TextBlock TextContent="{Binding Path=Age}" />
<TextBlock TextContent=")" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<!-- no need for an ItemTemplate setting -->
<ListBox ItemsSource="{Binding}" >
在示例4-26中,我们将数据模板的定义提升到资源模块,并且使用标签的DataType属性标志这个数据模板是类型化的。现在,除非另外通知,每当WPF看到Person对象的一个实例,就会应用相应的数据模板。这是一条便利之路,保证数据以一致的方式显示,遍及于你的应用程序,而不用担心显示的位置。
4.3.4列表的改变
迄今,我们已经得到一个对象的列表,我们可以适当的进行编辑,以及在其中建立导航,甚至轻而易举地高亮显示某些数据,以及提供了一个自动搜索,表现那些没有装载的来自厂商的数据。考虑到我们已经到达的程度,你可能怀疑提供一个Add按钮是一件轻而易举的事情,正如示例4-27所示。
示例4-27
void addButton_Click(object sender, RoutedEventArgs e) {
People people = (People)this.FindResource("Family");
people.Add(new Person("Chris", 35));
}
}
这个实现的问题在于,尽管视图可以判断出新条目的存在当你移动到这里的时候,而列表框本身却并不知道新增加的集合中的条目,正如图4-15。
图4-15
为了与图4-15显示的应用程序状态交互,我运行了这个程序,点击了Add按钮并使用Forward按钮导航到图中所示。然而,即使新人显示在文本框中,列表框仍然不知道添加了什么事物。同样地,如果有对象被删除,它也不会知道。就像数据绑定需要事先INotifyPropertyChanged接口,使用数据绑定的列表需要实现INotifyPropertyChanged这个接口,正如示例4-28。
示例4-28
public interface INotifyCollectionChanged {
event NotifyCollectionChangedEventHandler CollectionChanged;
}
}
INotifyCollectionChanged接口用于通知数据绑定控件,有条目在绑定列表中添加或删除。尽管在你的自定义类型中实现INotifyPropertyChanged,从而支持两种方式的数据绑定在你的类型化属性上——这很普通;不普通的是实现你自己的集合类,这些类给你很少的机会实现INotifyCollectionChanged接口。取代之,你更加更能依赖于集合类的一项在.NET 框架类库中,用来实现INotifyCollectionChanged。这样的类数量很少,而且不幸的是,我们使用的保持着Person对象的集合类,并不在其中。当你受欢迎的度过你的夜晚和周末实现了INotifyPropertyChanged,WPF提供了ObservableCollection<T>类,用于我们那些紧迫的职责,如示例4-29所示。
示例4-29
public class ObservableCollection<T> :
Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged {
}
}
既然ObservableCollection<T>派生于Collection<T>,而且实现了INotifyCollectionChanged接口,我们可以使用它代替List<T>作为我们的Person集合,正如示例4-30。
示例4-30
class Person : INotifyPropertyChanged {}
class People : ObservableCollection<Person> {}
}
现在,当一个条目添加到或删除自Person集合,这些变化将要在数据绑定列表中反映出来,正如图4-6所示。
图4-16
4.3.5排序
一旦我们适当地使数据目标每次显示多于一个事物,一个年轻人的爱好变得更多,当然,是喜欢的事物,正如对数据视图排序或者过滤。回忆视图经常位于数据绑定目标和数据源之间。这意味着可以越过我们不要显示的数据(被称为过滤,而且可以被直接覆盖),而且可以改变数据显示的顺序,又名排序。最简单的排序方法是通过操作视图的Sort属性,正如示例4-31所示。
示例4-31
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void sortButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
if( view.Sort.Count == 0 ) {
view.Sort.Add(
new SortDescription("Name", ListSortDirection.Ascending));
view.Sort.Add(
new SortDescription("Age", ListSortDirection.Descending));
}
else {
view.Sort.Clear( );
}
}
}
这里我们通过检测SortDescriptionCollection暴露在外的ICollectionView.Sort属性,将排序视图和未排序视图拴在一起。如果没有排序方式的描述,我们首先对Name属性按上升方式排序,然后对Age属性按下降方式排序。如果有排序方式的描述,我们将其清除,重新排序——无论之前是如何排序的。虽然排序描述在适当的位置,任意新添加到集合中的对象将被添加到它们已经排好序的适当位置,正如4-17所示。
一个SortDescription对象集合应该覆盖大多数的情形,但是如果你需要更多一点的控件,你可以提供自定义排序对象的视图,通过实现IComparer接口,正如示例4-32。
图4-17
示例4-32
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;
return rhs.Age - lhs.Age;
}
}
public partial class Window1 : Window {
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void sortButton_Click(object sender, RoutedEventArgs e) {
ListCollectionView view = (ListCollectionView)GetFamilyView( );
if( view.CustomSort == null ) {
view.CustomSort = new PersonSorter( );
}
else {
view.CustomSort = null;
}
}
}
在设置了自定义排序的情况,我们必须做一个假设——详细明确地实现了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
ICollectionView GetFamilyView( ) {
People people = (People)this.FindResource("Family");
return BindingOperations.GetDefaultView(people);
}
void filterButton_Click(object sender, RoutedEventArgs e) {
ICollectionView view = GetFamilyView( );
if( view.Filter == null ) {
view.Filter = delegate(object item) {
return ((Person)item).Age >= 18;
};
}
else {
view.Filter = null;
}
}
}
正如排序,通过使用一个恰当的过滤器,新条目被适当的过滤掉了,正如图4-18所示。
图4-18
图4-18中最上面的窗体显示了没有过滤器,中间的窗体显示了过滤了初始的列表,底部的窗体显示了添加一个成年人,过滤器仍然在恰当的位置。