21 WPF数据视图
视图对象
当你绑定集合到ItemsControl,在幕后数据视图被安静地创造。视图位于数据源和绑定控件之间。数据视图是通往数据源的一个窗口。它跟踪当前项目,它支持诸如排序,过滤,和分组特征。这些特征独立于数据对象本身,意味着你能以不同的方式、在窗口的不同部分(或应用的不同部分)绑定相同的数据。例如,你能绑定相同的产品集合到两个不同的列表但是过滤他们显示不同的记录。
视图对象依赖于数据对象的类型。所有的视图派生自CollectionView,但是两个特殊的实现派生自CollectionView:ListCollectionView和BindingListCollectionView。这是它如何工作:
- 如果数据源实现IBindingList,一个BindingListCollectionView被创造。当你绑定一个ADO.NET DataTable时发生。
- 如果数据源没有实现IBindingList但是它实现IList,一个ListCollectionView被创造。当你绑定一个ObservableCollection,如同产品的列表。
- 如果你的数据源没有实现IBindingList或IList但是它实现IEnumerable,你获得一个基本CollectionView。
取回一个视图对象
为获得一个目前使用的视图对象,你使用System.Windows.Data.CollectionViewSource类的GetDefaultView()静态方法。当你调用GetDefaultView(),并传递数据源,就是你正使用的集合。这是一个例子,获得绑定到列表的产品集合的视图:
ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
GetDefaultView()方法总返回一个ICollectionView引用。你需要根据数据源转换视图对象到合适的类,可能是ListCollectionView或BindingListCollectionView。
var view = (ListCollectionView)
CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
用视图导航
视图对象决定列表项目的数目(Count属性)和获得当前的数据对象一个引用(CurrentItem)或当前的位置索引(CurrentPosition)。也能使用几个方法从一记录移动到另一个,诸如MoveCurrentToFirst(),MoveCurrentToLast(),MoveCurrentToNext(),MoveCurrentToPrevious(),和MoveCurrentToPosition()。
显示绑定产品数据的绑定文本框保持不变。他们只需要指明合适的属性,如下所示:
<TextBlock Margin="7">Model Number:</TextBlock>
<TextBox Margin="5" Grid.Column="1" Text="{Binding Path=ModelNumber}"></TextBox>
但是,这例子没有包括任何列表控件,所以你要控制导航。为简化生活,你能在你的窗口类添加一个成员变量,存储指向视图的一个引用:
private ListCollectionView view;
在这种情况下,代码转换视图到合适的视图类型(ListCollectionView)而不是使用ICollectionView接口。ICollectionView接口提供了大多数功能,但是它缺乏Count属性。
当窗口第一次加载,你能获得数据,放置它到窗口的DataContext,和存储一个引用指向视图:
var products = App.StoreDB.GetProducts();
this.DataContext = products;
view = (ListCollectionView)
CollectionViewSource.GetDefaultView(this.DataContext);
view.CurrentChanged += new EventHandler(view_CurrentChanged);
第二行在DataContext中放置产品对象的完整集合。绑定控件将沿元素树向上搜索,直到他们发现这个对象。当然,你希望绑定表达式绑定到集合的当前项目,而不是绑定到集合本身,但是WPF足够聪明能自动地推算。它自动地提供他们当前项目,所以你不需要额外的代码的一个缝合。
前一个例子有一附加的代码语句。它连接一个事件处理器到视图的CurrentChanged事件。当事件发生,你能执行几个有用的行为,诸如前一个和下一个按钮依赖于当前位置可用或不可用,和在窗口底部的TextBlock显示当前位置。
private void view_CurrentChanged(object sender, EventArgs e)
{
lblPosition.Text = "Record " + (view.CurrentPosition + 1).ToString() +
" of " + view.Count.ToString();
cmdPrev.IsEnabled = view.CurrentPosition > 0;
cmdNext.IsEnabled = view.CurrentPosition < view.Count - 1;
}
最后一步是写前一个和下一个按钮的逻辑。因为当这些按钮不能应用时,自动地不可用。你不需要考虑可能会移动到第一个项目之前或最后一个项目之后。
private void cmdNext_Click(object sender, RoutedEventArgs e)
{
view.MoveCurrentToNext();
}
private void cmdPrev_Click(object sender, RoutedEventArgs e)
{
view.MoveCurrentToPrevious();
}
你能添加一个组合框到窗口,用于直接跳到某一记录。
<ComboBox Name="lstProducts" DisplayMemberPath="ModelName"
Text="{Binding Path=ModelName}"
SelectionChanged="lstProducts_SelectionChanged"></ComboBox>
指定数据源:
lstProducts.ItemsSource = products;
默认情况下,ItemsControl的当前项目不与视图的当前项目同步。幸运地,有两个容易的方法解决问题。
第一个用传统的代码方式强制同步:
private void lstProducts_SelectionChanged(object sender, RoutedEventArgs e)
{
view.MoveCurrentTo(lstProducts.SelectedItem);
}
一个更简单解决方案是设置ItemsControl.IsSynchronizedWithCurrentItem为真。那样,目前选择项目自动地同步匹配视图的当前位置。
使用查询表帮助编辑
组合框能方便地编辑记录值。
例如,你可能有数据库一个字段接受几个预置值之一。在这种情况下,使用一个组合框,绑定它到合适的字段,在Text属性上使用一个绑定表达式。但是,填充组合框用容许的值,依靠设置它的ItemsSource属性指向你定义列表。并且如果你希望显示列表值一方式(例如,为文本)但是存储他们另一个方式(为数字编码),只要添加一个值转换器到你的Text属性绑定。
另一个情况是相关表。例如,你可能希望允许用户拾一个产品目录使用定义所有的目录列表。基本方法是相同的:设置Text属性绑定合适的字段,和用ItemsSource属性填充选项列表。如果你需要转换低层的IDs到更有意义的名字,使用一个值转换器。
用声明方式创造一个视图
你能在XAML标记以声明方式构造一个CollectionViewSource,和然后绑定CollectionViewSource到你的控件(诸如列表)。
从技术上,CollectionViewSource不是一个视图。它是一个帮助者类,允许你取回一个视图(使用GetDefaultView()方法)和一个工厂,能创造一个视图。
CollectionViewSource类的二最重要的属性是View,包裹视图对象,和Source,包裹数据源。CollectionViewSource也添加SortDescriptions和GroupDescriptions属性,这镜像同一地命名视图属性。当CollectionViewSource创造一个视图,它简单地传递这些属性的值到视图。
CollectionViewSource也包含一个Filter事件,你能处理执行过滤。这过滤工作方式等同于视图对象提供的过滤回调,除了它被定义为一个事件,所以你能容易地在XAML中挂钩上你的事件处理器。
例如,考虑前一个例子,使用价格范围这对产品分组。这是你如何以声明方式定义转换器和CollectionViewSource:
<local:PriceRangeProductGrouper x:Key="Price50Grouper" GroupInterval="50"/>
<CollectionViewSource x:Key="GroupByRangeView">
<CollectionViewSource.SortDescriptions>
<component:SortDescription PropertyName="UnitCost" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="UnitCost"
Converter="{StaticResource Price50Grouper}"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
注意,SortDescription类不是WPF名字空间。为了使用它,你需要填加下面的名字空间别名:
xmlns:component="clr-namespace:System.ComponentModel;assembly=WindowsBase"
一旦你建立CollectionViewSource,你能绑定它到你的列表:
<ListBox ItemsSource="{Binding Source={StaticResource GroupByRangeView}}" ... >
似乎列表框控件绑定到CollectionViewSource,而不是CollectionViewSource暴露的视图(这被存储在CollectionViewSource.View属性)。但是,WPF数据绑定对于CollectionViewSource一个特殊的例外。当你使用它在一个绑定表达式,WPF请求CollectionViewSource创造它的视图,然后绑定视图到合适的元素。
声明式的方法没有真正地节省你任何工作。你仍然需要在运行时用代码取回数据。不同的是现在你的代码必须传递数据沿着到CollectionViewSource而不是直接提供它到列表:
var products = App.StoreDB.GetProducts();
var viewSource = (CollectionViewSource)
this.FindResource("GroupByRangeView");
viewSource.Source = products;
可选地,你能使用XAML标记创造产品集合作为一个资源。然后你能以声明方式绑定CollectionViewSource到你的产品集合。但是,你仍然需要使用代码填充你的产品集合。
过滤、排序、和分组
视图跟踪数据对象集合的当前位置。这是一个重要的任务,和发现(或改变)当前项目是使用视图的最普遍原因。
视图也提供若干可选的特征那允许你管理项目的全体集合。在下几节中,你将会看到你能如何使用一个视图过滤你的数据项目(暂时地隐藏那些你不希望看见),你能如何使用它应用排序(改变数据项目顺序),和你能如何使用它应用分组(创造能被独立地导航子集合)。
过滤集合
过滤允许你显示满足特定条件的一个子集。当带有一个集合作为数据源工作时,你使用视图对象的Filter属性设置过滤。
Filter属性的实现有点笨拙。它接受一个Predicate委托指向一个自定义过滤方法(你创造)。这是一个例子,你能如何连接视图到方法FilterProduct():
var view = (ListCollectionView)
CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);
view.Filter = new Predicate<object>(FilterProduct);
笨拙之处在于你只能使用Predicate