View对象

  当将集合(或 DataTable)绑定到1temsControl控件时,会不加通告地在后台创建数据视图——位于数据源和绑定的控件之间。**数据视图是进入数据源的窗口,可以跟踪当前项,并且支持各种功能,如排序、过滤以及分组。**这些功能和数据对象本身是相互独立的,这意味着可在窗口的不同部分(或应用程序的不同部分)使用不同的方式绑定相同的数据。例如,可将同一产品集合绑定到两个不同的列表,并对产品进行过滤以显示不同的记录。

  使用的视图对象取决于数据对象的类型。所有视图都继承自CollectionView类,并且有两个继承自CollectionView类的特殊实现:ListCollectionViewBeginListCollectionView。下面是CollectionView类的工作原理:

  • 如果数据源实现了IBindingList接口,就会创建BindingListCollectionView视图。当绑定到ADO.NET中的DataTable对象时会创建该视图
  • 如果数据源没有实现IBindingList接口,但实现了IList接口,就会创建ListCollectionView视图。当绑定到ObservableCollection集合时会创建该视图
  • 如果数据源没有实现IBindingListIList接口,当实现了IENumberable接口,就会得到基本的COllectionView视图

在理想情况下,应避免第三种情况。对于大量的项和修改数据源的操作(如插入和删除),CollectionView 视图的性能不佳。如果不是绑定到 ADO.NET 数据对象,使用ObservableCollection类几乎总是最简单的方法。

检索视图对象

  为得到当前使用的视图对象,可使用 System,Windows.Data.CollectionViewSource 类的GetDefaultView()静态方法。当调用 GetDefaultView()方法时,传入数据源——正在使用的集合或 DataTable 对象。下面的示例获取绑定到列表的产品集合的视图:

1
ICollectionView view = CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);

  GetDefaultView()方法总是返回一个 ICollectionView引用,所以您需要根据数据源,将视图对象转换为合适的类,如ListCollectionView或BindingListCollectionView。

1
ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(lstProducts.ItemsSource);

视图导航

  可使用视图对象完成的最简单一件事是确定列表中的项数(通过 Count属性),以及获取当前数据对象的引用(通过 CurrentItem属性)或当前位置索引(通过 CurrentIndex 属性)。还可以用少数方法从一条记录移到另一条记录,如MoveCurrentToFirst()、MoveCurrentToLast()、MoveCurrentToNext()、MoveCurrentToPrevious()以及 MoveCurrentToPosition()。到目前为止,您还不需要这些细节,因为现在看到的所有示例都是通过使用列表,让用户可从一条记录移到下一条记录。但是如果希望创建记录浏览器应用程序,就可能希望提供自己的导航按钮。下面显示了一个例子。

传统做法(推荐通过IsSynchronizedWithCurrentItem设置选择项自动同步)

设置选中项自动同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
<Window
Height="375"
Title="MainWindow"
Width="375"
x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Border Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<ComboBox
DisplayMemberPath="ModelName"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Products}"
x:Name="LstProducts" />

<Border
Background="LightSteelBlue"
Grid.Row="1"
Margin="0,10,0,0"
Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Grid.Row="0"
Margin="0,5"
Text="Model Number" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelNumber}" />
<TextBlock
Grid.Column="0"
Grid.Row="1"
Margin="0,5"
Text="Model Name" />
<TextBox
Grid.Column="1"
Grid.Row="1"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelName}" />
<TextBlock
Grid.Column="0"
Grid.Row="2"
Margin="0,5"
Text="Unit Cost" />
<TextBox
Grid.Column="1"
Grid.Row="2"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.UnitCost}" />
<TextBlock
Grid.Column="0"
Grid.Row="3"
Text="Description" />
<TextBox
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,5,0,0"
MinHeight="120"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.Description}" />
</Grid>
<StackPanel
Grid.Row="1"
Margin="0,10,0,0"
Orientation="Horizontal">
<Button Command="{Binding PreviousCommand}">&lt;</Button>
<TextBlock Margin="5,0" Text="{Binding DisplayPosition}" />
<Button Command="{Binding NextCommand}">&gt;</Button>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
</Window>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private string? _displayPosition;

[ObservableProperty]
private List<Product> _products =
[
new Product()
{
ModelNumber = "RU007",
ModelName = "Rain Racer 2000",
UnitCost = 120,
Description = "Liiks like an ordianry bumbershoot....."
},
new Product()
{
ModelNumber = "RU008",
ModelName = "iPhone 16 Pro",
UnitCost = 1520,
Description = "I don't use....."
},
new Product()
{
ModelNumber = "RU009",
ModelName = "Huawei Mate 60 Pro",
UnitCost = 12542,
Description = "huawei is good....."
},
new Product()
{
ModelNumber = "RU0010",
ModelName = "Xiao Mi 13",
UnitCost = 1054,
Description = "this phone is very good....."
},
new Product()
{
ModelNumber = "RU0011",
ModelName = "Vivo IQOO 5",
UnitCost = 199,
Description = "I don't know....."
},
new Product()
{
ModelNumber = "RU0012",
ModelName = "Xiao Mi SU7",
UnitCost = 747545,
Description = "My dream car....."
},
];

private readonly ListCollectionView _view;

public MainWindowViewModel()
{
_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
_view.CurrentChanged += ViewOnCurrentChanged;
}

private void ViewOnCurrentChanged(object? sender, EventArgs e)
{
DisplayPosition = $"Record {_view.CurrentPosition + 1} of {_view.Count}";
NextCommand.NotifyCanExecuteChanged();
PreviousCommand.NotifyCanExecuteChanged();
}

private bool CanPrevious() => _view.CurrentPosition > 0;
private bool CanNext() => _view.CurrentPosition < _view.Count - 1;

[RelayCommand(CanExecute = nameof(CanPrevious))]
private void Previous()
{
_view.MoveCurrentToPrevious();
}

[RelayCommand(CanExecute = nameof(CanNext))]
private void Next()
{
_view.MoveCurrentToNext();
}
}

  粗鲁的强制方法是无论何时在列表中选择一条记录,都简单地移动到新记录(_view.MoveCurrentTo(product))。更简单的方法是,将ItemsControl.IsSynchronizedWithCurrentItem属性设置为true。使用这种方法,当前选择的项会被自动同步,从而匹配视图的当前位置,而且不需要使用任何代码。

以声明方式创建视图

  个在XAML标记中以声明方式构建CollectionViewSource对象,然后将CollectionViewSource对象绑定到控件(如列表控件)。

从技术角度看,CollectionViewSource不是视图,而是用于检索视图的辅助类(例如GetDefaultView()方法)。当需要使用时,甚至是能够创建视图的工厂

  CollectionViewSource类中有两个重要的属性时ViewSource,其中View属性封装了视图对象,Source属性封装了数据源。CollectionViewSource类还添加了SortDescriptionsGroupDescriptions属性,这两个属性镜像了前面介绍过的同名视图睡醒。当使用CollectionViewSource类创建视图时,只是将这些属性的值传给视图。

  CollectionViewSource 类还提供了 Filter 事件,用于执行过滤。除被定义为事件从而可以很容易地使用 XAML关联事件处理程序外,Filter事件的工作方式与视图对象提供的 Filter 回调函数的工作方式相同。

  例如,考虑前面的示例,该例使用价格范围对产品进行分组。在此以声明方式定义这个示例所需的转换器和CollectionViewSource对象:

  用于分组的转换器,例如示例中通过商品价格进行分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PriceRangeProductGrouperConverter : MarkupExtension, IValueConverter
{
public int GroupInterval { get; set; }

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
decimal price = System.Convert.ToDecimal(value);
if (price < GroupInterval)
{
return string.Format(culture, "Less than {0:C}", GroupInterval);
}
else
{
int interval = (int)price / GroupInterval;
int lowerLimit = interval * GroupInterval;
int upperLimit = (interval + 1) * GroupInterval;
return string.Format(culture, "{0:C} to {1:C}", lowerLimit, upperLimit);
}
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}

  通过XAML声明的方式定义CollectionViewSource视图,并声明排序(SortDescriptions)和分组(GroupDescripts);列表中通过GroupStyle可以显示列表的分组描述信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<Window
Height="375"
Title="MainWindow"
Width="375"
x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:component="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:cvt="clr-namespace:WpfApp.Converters"
xmlns:local="clr-namespace:WpfApp.ViewModels"
xmlns:m="clr-namespace:WpfApp.Models"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<cvt:PriceRangeProductGrouperConverter GroupInterval="50" x:Key="Price50Grouper" />
<CollectionViewSource x:Key="GroupByRangeView">
<CollectionViewSource.SortDescriptions>
<component:SortDescription Direction="Ascending" PropertyName="UnitCost" />
</CollectionViewSource.SortDescriptions>
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription Converter="{StaticResource Price50Grouper}" PropertyName="UnitCost" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>

</Window.Resources>
<Grid>
<ListBox HorizontalContentAlignment="Stretch" ItemsSource="{Binding Source={StaticResource GroupByRangeView}}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type m:Product}">
<Grid>
<TextBlock HorizontalAlignment="Left" Text="{Binding ModelName}" />
<TextBlock HorizontalAlignment="Right" Text="{Binding UnitCost, StringFormat=C}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>

  实际上,声明式方法并没有节省任何工作。在运行时仍需要便携代码男检索数据。区别是现在代码必须通过CollectionViewSource对象传递数据,而不能直接为列表提供数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();

ICollection<Product> products = _products; // Mock Fetch Data
CollectionViewSource viewSource = (CollectionViewSource)this.FindResource("GroupByRangeView");
viewSource.Source = products;
}

private List<Product> _products =
[
new Product()
{
ModelNumber = "RU007",
ModelName = "Rain Racer 2000",
UnitCost = 58,
Description = "Liiks like an ordianry bumbershoot....."
},
new Product()
{
ModelNumber = "RU008",
ModelName = "iPhone 16 Pro",
UnitCost = 23,
Description = "I don't use....."
},
new Product()
{
ModelNumber = "RU009",
ModelName = "Huawei Mate 60 Pro",
UnitCost = 17,
Description = "huawei is good....."
},
new Product()
{
ModelNumber = "RU0010",
ModelName = "Xiao Mi 13",
UnitCost = 99,
Description = "this phone is very good....."
},
new Product()
{
ModelNumber = "RU0011",
ModelName = "Vivo IQOO 5",
UnitCost = 5,
Description = "I don't know....."
},
new Product()
{
ModelNumber = "RU0012",
ModelName = "Xiao Mi SU7",
UnitCost = 49,
Description = "My dream car....."
},
];

}

  实际上,声明式方法并没有节省任何工作。在运行时仍需要便携代码男检索数据。区别是现在代码必须通过CollectionViewSource对象传递数据,而不能直接为列表提供数据。

  现在,您已经看到了如何使用基于代码和标记的方法配置视图,您可能会好奇哪种方法是更好的设计决定。其实,这两种方法都可以。应该根据希望在什么位置集中放置数据视图的细节来决定使用哪种方法。
  但如果希望使用多视图,选用哪种方法就变得更加重要了。对于这种情况,最好在标记中定义所有视图,然后使用代码在适当的视图中进行交换。

过滤、排序与分组

  视图跟踪数据对象集合中的当时位置。这是一项重要的任务,而且查找或修改当前项是使用视图最常见的原因。

  视图还提供了许多可选功能,通过这些功能个管理整个数据项集合,例如过滤、排序、分组

过滤集合

  通过过滤个显示复合特定条件的记录子集。在将集合用作数据源时,可使用视图对象的Filter属性设置过滤器。
  Filter属性的实现有些笨拙。他接受一个Predicate委托,该委托指向自定义的过滤方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<Window
Height="375"
Title="MainWindow"
Width="450"
x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".4*" />
<ColumnDefinition Width=".6*" />
</Grid.ColumnDefinitions>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox
DisplayMemberPath="ModelName"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Products}"
x:Name="LstProducts" />
<Border
BorderBrush="Gray"
BorderThickness="1"
Grid.Row="1"
Padding="5">
<StackPanel>
<DockPanel>
<TextBlock Margin="0,0,8,0" Text="Price &gt; " />
<TextBox Text="{Binding MinimumPrice}" />
</DockPanel>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
<Button
Command="{Binding RefreshCommand}"
Content="Refresh"
Margin="0,0,8,0"
Padding="5,0" />
<Button
Command="{Binding RemoveFilterCommand}"
Content="Remove Filter"
Padding="5,0" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
<Border
Background="LightSteelBlue"
Grid.Column="1"
Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Grid.Row="0"
Margin="0,5"
Text="Model Number" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelNumber}" />
<TextBlock
Grid.Column="0"
Grid.Row="1"
Margin="0,5"
Text="Model Name" />
<TextBox
Grid.Column="1"
Grid.Row="1"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelName}" />
<TextBlock
Grid.Column="0"
Grid.Row="2"
Margin="0,5"
Text="Unit Cost" />
<TextBox
Grid.Column="1"
Grid.Row="2"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.UnitCost}" />
<TextBlock
Grid.Column="0"
Grid.Row="3"
Text="Description" />
<TextBox
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,5,0,0"
MinHeight="120"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.Description}" />
</Grid>
</Border>
</Grid>
</Window>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private decimal _minimumPrice;

[ObservableProperty]
private List<Product> _products =
[
new Product()
{
ModelNumber = "RU007",
ModelName = "Rain Racer 2000",
UnitCost = 120,
Description = "Liiks like an ordianry bumbershoot....."
},
new Product()
{
ModelNumber = "RU008",
ModelName = "iPhone 16 Pro",
UnitCost = 1520,
Description = "I don't use....."
},
new Product()
{
ModelNumber = "RU009",
ModelName = "Huawei Mate 60 Pro",
UnitCost = 12542,
Description = "huawei is good....."
},
new Product()
{
ModelNumber = "RU0010",
ModelName = "Xiao Mi 13",
UnitCost = 1054,
Description = "this phone is very good....."
},
new Product()
{
ModelNumber = "RU0011",
ModelName = "Vivo IQOO 5",
UnitCost = 199,
Description = "I don't know....."
},
new Product()
{
ModelNumber = "RU0012",
ModelName = "Xiao Mi SU7",
UnitCost = 747545,
Description = "My dream car....."
},
];

private readonly ListCollectionView _view;

public MainWindowViewModel()
{
_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
}

[RelayCommand]
private void Refresh()
{
ProductByPriceFilter filter = new ProductByPriceFilter(MinimumPrice);
_view.Filter = (filter.FilterItem);
_view.Refresh();
}

[RelayCommand]
private void RemoveFilter()
{
_view.Filter = null;
}
}

  过滤器检査集合中的每个数据项,并且如果被检査的项满足过滤条件,就返回tue,否则则返回 false。当创建 Predicate 对象时,指定进行检査的对象类型。笨拙的部分是视图期望用户使用 Predicate实例–您不能使用更有用的内容(如 Predicate),这样就不可避免地使用类型转换代码。

过滤DataTable对象

  对于 DataTable 对象,过滤工作是不同的。如果以前使用过 ADO.NET,可能已经知道每个DataTable 对象都与一个 DataView 对象相关联(和 DataTable 对象一样,DataView 对象和其他ADO.NET 核心数据对象一起在 System.Data 名称空间中进行定义)。ADO.NET 中的 DataView对象扮演和 WPF 视图对象相同的角色。与 WPF 视图一样,可使用 DataView 对象过滤记录(使用 RowFiter属性通过字段内容进行过滤,或者使用 RowStateFilter 属性通过行状态进行过滤)。DataView 对象还通过 Sort属性支持排序。与 WPF视图对象不同的是,DataView 对象不跟踪数据集中的位置。DataView 对象还提供了用于锁定编辑能力的附加属性(AllowDelete、AllowEdit以及 AllowNew)。

  可通过检索绑定的 DataView 对象并直接修改其属性来改变过滤数据列表的方式(请记住,可通过 DataTable.DefaultView 属性获取默认的 DataView 对象)。不过,如能通过 WPF 视图对象调整过滤会更好,因为这样可以继续使用相同的模型。

  这是可能的,但存在一些局限性。与 ListCollectionView 视图不同,DataTable 对象使用的BindingListCollectionView 视图不支持 Filter 属性(BindingListCollectionView.CanFilter 属性返回false,并且如果试图设置 Filter 属性,就会导致抛出异常)。相反,BindingListCollectionView 视图提供了 CustomFilter 属性。CustomFilter 属性本身不能做任何工作——只是接收指定的过滤字符串,并使用这个过滤字符串设置 DataView.RowFilter属性。

  **使用 DataView.RowFilter 属性非常容易,但有点混乱。将基于字符串的过滤器表达式作为参数,这个表达式类似于 SELECT 查询中用于构造 WHERE 子句的 SQL代码段。**因此,需要遵循所有的 SQL约定。例如,使用单引号()将字符串和日期值括起来。并且如果希望使用多个条件,还需要使用 OR 和 AND 关键字将这些条件结合在一起。

下面的示例复制了前面基于集合的示例中的过滤,并将该过滤用于产品记录的DataTable对象:

1
2
BindingListCollectionView view = CollectionViewSource.GetDefaultView(_products);
view.CustomFilter = $"UnitCost > {MinimumPrice}";

排序

  还可以使用视图进行排序。最简单的方法是根据每个数据项中的一个或多个属性的值进行排序。使用 System.ComponentModel.SortDescription 对象确定希望使用的字段。每个 SortDescription对象确定希望用于排序的字段和排序方向(升序或降序)。按照希望应用它们的顺序添加SortDescription对象。例如,可首先根据类别进行排序,然后再根据型号名称进行排序。

下面的示例根据型号名称进行简单的升序排序:

1
2
ICollectionView view = CollectionViewSource.GetDefaultView(_products);
view.SortDescriptsions.Add(new SortDescription("ModelName", ListSortDirection.Ascending));

  因为上面的代码使用的是 ICollectionView 接口(而不是特殊的视图类),所以能够正常工作,而不管绑定的是什么类型的数据源。如果使用的是 BindingListCollectionView 视图(当绑定到DataTable 时),SortDescription 对象用于构建应用于 DataView.Sort 属性的排序字符串。

当对字符串进行排序时,按照字母顺序对值进行排序。数字按照数字顺序进行排序。为应用不同的排序顺序,首先要清除已经存在的SortDescriptions 集合

  **还可执行自定义排序,但只能用于 ListCollectionView 视图(不能用于 BindingListCollection-View 视图)。ListCollectionView 类提供的 CustomSort属性接收一个IComparer 对象,IComparer对象在两个数据项之间进行比较,并且指示较大项。**如果需要构建通过组合多个属性来得到排序键的排序例程,这种方法是非常方便的。如果需要使用非标准的排序规则,这种方法也很有意义。例如,在排序之前可能希望忽略产品代码的前面几个字符、对价格进行一些计算、将字段转换为不同的数据类型或不同的表示形式等。下面的示例计算产品型号名称中字母的数量,并使用该数量确定排序的顺序:

1
2
ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
view.CustomSort = new SortByModelNameLength();

分组

  与支持排序的方式相同,视图也支持分组。与排序一样,可使用简单的方式进行分组(根据单个属性值),也可以使用复杂的方式进行分组(使用自定义的回调函数)。

  为执行分组,需要为CollectionView.GroupDescriptions集合添加System.ComponentModel.PropertyGroupDescription对象。下面的示例根据类别名称进行分组:

1
2
ICollectionView view = CollectionViewSource.GetDefaultView(_products);
view.GroupDescriptions.Add(new PropertyGroupDescription("CategoryName"));

该例假定 Product 类有名为 CategoryName 的属性。您很可能有名为 Category 的属性(该属性返回链接的 Category对象)或名为 CategoryID的属性(该属性使用唯一的ID数字来标识类别)。在这些情况下,仍可以使用分组,但需要添加检查分组信息(如 Category 对象或 CategoryID 属性)的值转换器,并返回正确的用于分组的类别文本。下一个示例将介绍如何使用值转换器进行分组。

  可使用样式为列表中的所有 Groupltem 对象应用格式。然而,您不仅可能希望调整格式–例如,可能还希望显示分组标题,这就需要使用模板。幸运的是,ItemsControl类通过它的ItemsControl.GroupStyle 属性简化了这两项任务,该属性提供了一个 GroupStyle 对象的集合。虽然属性名称中包含了“Style”,但 GroupStyle 类并不是样式。它只是一个简便的包,为配置Groupltem 对象封装了一些有用的设置。下表列出了 GroupStyle 类的属性。

GroupStyle类的属性

|名称|说明|
|ContainerStyle|设置被应用到为每个分组生成的GroupItem元素的样式|
|ContainerStyleSelector|不是使用ContainerStyle属性,而是使用ContainerStyleSelector属性提供一个类,该类根据分组选择准备使用的正确样式|
|HeaderTemplate|允许用户为了在每个分组开头显示内容而创建模板|
|HeaderTemplateSelector|不是使用HeaderTemplate属性,而是使用HeaderTemplateSelector属性提供一个类,该类根据分组选用正确的头模板|
|Panel|改变用于包含分组的模板。例如,个使用WrapPanel面板而非标准的StackPanel面板,创建成左向右然后向下平铺的列表|

在下例中,需要做的所有工作就是在每个分组前显示一个标题。个使用这种技术创建如下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<Window
Height="375"
Title="MainWindow"
Width="450"
x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".4*" />
<ColumnDefinition Width=".6*" />
</Grid.ColumnDefinitions>
<Grid>
<ListBox
DisplayMemberPath="ModelName"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Products}"
x:Name="LstProducts">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
</Grid>
<Border
Background="LightSteelBlue"
Grid.Column="1"
Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Grid.Row="0"
Margin="0,5"
Text="Model Number" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelNumber}" />
<TextBlock
Grid.Column="0"
Grid.Row="1"
Margin="0,5"
Text="Model Name" />
<TextBox
Grid.Column="1"
Grid.Row="1"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelName}" />
<TextBlock
Grid.Column="0"
Grid.Row="2"
Margin="0,5"
Text="Unit Cost" />
<TextBox
Grid.Column="1"
Grid.Row="2"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.UnitCost}" />
<TextBlock
Grid.Column="0"
Grid.Row="3"
Text="Description" />
<TextBox
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,5,0,0"
MinHeight="120"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.Description}" />
</Grid>
</Border>
</Grid>
</Window>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty]
private List<Product> _products =
[
new Product()
{
ModelNumber = "RU007",
ModelName = "Rain Racer 2000",
UnitCost = 120,
CategoryName = "Tool",
Description = "Liiks like an ordianry bumbershoot....."
},
new Product()
{
ModelNumber = "RU008",
ModelName = "iPhone 16 Pro",
CategoryName = "Phone",
UnitCost = 1520,
Description = "I don't use....."
},
new Product()
{
ModelNumber = "RU009",
ModelName = "Huawei Mate 60 Pro",
UnitCost = 12542,
CategoryName = "Phone",
Description = "huawei is good....."
},
new Product()
{
ModelNumber = "RU0010",
ModelName = "Xiao Mi 13",
CategoryName = "Phone",
UnitCost = 1054,
Description = "this phone is very good....."
},
new Product()
{
ModelNumber = "RU0011",
ModelName = "Vivo IQOO 5",
UnitCost = 199,
CategoryName = "Phone",
Description = "I don't know....."
},
new Product()
{
ModelNumber = "RU0012",
ModelName = "Xiao Mi SU7",
UnitCost = 747545,
CategoryName = "Car",
Description = "My dream car....."
},
];

private readonly ListCollectionView _view;

public MainWindowViewModel()
{
_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
_view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(Product.CategoryName)));
}
}

  您可能希望组合使用分组和排序。如果希望对分组进行排序,只需要确保用于排序的第SortDescription 对象基于分组字段即可。下面的代码根据类别名称按字母顺序对类别进行排个序,然后根据型号名称对类别中的每个产品按字母顺序进行排序。

1
2
3
_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
_view.SortDescriptions.Add(new SortDescription(nameof(Product.CategoryName), ListSortDirection.Ascending));
_view.SortDescriptions.Add(new SortDescription(nameof(Product.ModelName), ListSortDirection.Ascending));

范围分组

  **使用在此看到的这种简单分组方法的局限性在于,为了进行分组,分组中的记录需要有一个具有相同数值的字段。**上面的示例之所以能够工作,是因为许多产品共享相同的类别并为CategoryName 属性使用相同的数值。但如果试图根据其他信息进行排序,如UnitCost 字段,这种方法就不能起作用。对于这种情况,将为每个产品构建一个分组。

  可采用一种方法解决这个问题。可创建一个类,检查一些信息并为了显示目的而将它放置到一个概念组中。这种技术通常用于使用特定范围内的数字或日期信息对数据对象进行分组。 例如,可为小于 50 美元的产品创建一个组,为 50 美元和 100 美元之间的产品创建另一个组,等等。

  为创建这个解决方案,需要提供值转换器,检查数据源中的一个字段(或多个字段,如果实现了IMultiValueConverter接口),并返回组标题.只要为多个数据对象使用相同的组标题,这些对象就会放到相同的逻辑分组中。

下面的代码显示了创建下图所示的价格范围的值转换器。该转换器的设计考虑了一定的灵活性——可以制定分组范围的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class PriceRangeProductGrouperConverter : MarkupExtension, IValueConverter
{
public int GroupInterval { get; set; }

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
decimal price = System.Convert.ToDecimal(value);
if (price < GroupInterval)
{
return string.Format(culture, "Less than {0:C}", GroupInterval);
}
else
{
int interval = (int)price / GroupInterval;
int lowerLimit = interval * GroupInterval;
int upperLimit = (interval + 1) * GroupInterval;
return string.Format(culture, "{0:C} to {1:C}", lowerLimit, upperLimit);
}
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<Window
Height="375"
Title="MainWindow"
Width="450"
x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfApp.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".4*" />
<ColumnDefinition Width=".6*" />
</Grid.ColumnDefinitions>
<Grid>
<ListBox
DisplayMemberPath="ModelName"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Products}"
x:Name="LstProducts">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
</Grid>
<Border
Background="LightSteelBlue"
Grid.Column="1"
Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Grid.Row="0"
Margin="0,5"
Text="Model Number" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelNumber}" />
<TextBlock
Grid.Column="0"
Grid.Row="1"
Margin="0,5"
Text="Model Name" />
<TextBox
Grid.Column="1"
Grid.Row="1"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.ModelName}" />
<TextBlock
Grid.Column="0"
Grid.Row="2"
Margin="0,5"
Text="Unit Cost" />
<TextBox
Grid.Column="1"
Grid.Row="2"
Margin="0,5"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.UnitCost}" />
<TextBlock
Grid.Column="0"
Grid.Row="3"
Text="Description" />
<TextBox
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="4"
Margin="0,5,0,0"
MinHeight="120"
Text="{Binding ElementName=LstProducts, Path=SelectedItem.Description}" />
</Grid>
</Border>
</Grid>
</Window>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public partial class MainWindowViewModel : ObservableObject
{
[ObservableProperty] private List<Product> _products =
[
new Product()
{
ModelNumber = "RU007",
ModelName = "Rain Racer 2000",
UnitCost = 120,
CategoryName = "Tool",
Description = "Liiks like an ordianry bumbershoot....."
},
new Product()
{
ModelNumber = "RU008",
ModelName = "iPhone 16 Pro",
CategoryName = "Phone",
UnitCost = 1520,
Description = "I don't use....."
},
new Product()
{
ModelNumber = "RU009",
ModelName = "Huawei Mate 60 Pro",
UnitCost = 12542,
CategoryName = "Phone",
Description = "huawei is good....."
},
new Product()
{
ModelNumber = "RU0010",
ModelName = "Xiao Mi 13",
CategoryName = "Phone",
UnitCost = 1054,
Description = "this phone is very good....."
},
new Product()
{
ModelNumber = "RU0011",
ModelName = "Vivo IQOO 5",
UnitCost = 199,
CategoryName = "Phone",
Description = "I don't know....."
},
new Product()
{
ModelNumber = "RU0012",
ModelName = "Xiao Mi SU7",
UnitCost = 747545,
CategoryName = "Car",
Description = "My dream car....."
},
];

private readonly ListCollectionView _view;

public MainWindowViewModel()
{
_view = (ListCollectionView)CollectionViewSource.GetDefaultView(_products);
_view.SortDescriptions.Add(new SortDescription(nameof(Product.UnitCost), ListSortDirection.Ascending));

var grouper = new PriceRangeProductGrouperConverter();
grouper.GroupInterval = 1000;
_view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(Product.UnitCost),grouper));
}
}

分组和虚拟化

  之前接受啊了虚拟化,该功能降低了控件的内存开销,而且在绑定极长的列表时提神了速度。但是,即使控件支持虚拟化,也不会在启用虚拟化时使用。WPF使用新的VirtualizingStackPanel.IsVirtualizingWhenGroup属性纠正了这个问题。将其设置为true,分组列表将与为分组列表获得相同的虚拟化性能提升效果:

1
<ListBox VirtualizingStackPanel.IsVirtualizingWhenGrouping="True" ...>

  但在集合使用分组和长列表时仍需要谨慎,因为分组数据时会导致速度明显缓慢。因此,在实现此设计前需要对应用程序进行性能分析。\

实时成型

  如果改变正在使用的视图的过滤、排序或分组,就需要调用ICollectionViewSource.Refresh()方法男刷新视图,并确保正确的项出现在列表中。

  然而,一些更改捕获起来稍困难一些。当您更改视图时,容易记住刷新视图,但当应用程序中某处的代码例程更改数据时,会出现什么情况呢?例如,假设某个编辑操作将产品价格降至视图过滤条件需要的最低值以下。从技术角度看,这会导致记录从当前视图中消失,但除非您记得强制执行更新,否则看不到任何更改。

  WPF4.5引入了一项称为“实时成型”的功能,从而填补了这项空白。成本质来讲,实时成型监视特定属性中的变化。如果发现变化(如降低了Product对象上的价格),就确定相应公开会影响当前视图并出发刷新操作。

要使用实时成型,需要满足以下三项标准:

  • 数据对象必须实现INotifyPropertyChanged。当属性变化时,使用该接口发出通知
  • 集合必须实现ICollectionViewLiveShaping。标准的ListCollectionViewBindingListCollectionView类都实现ICollectionViewLiveShaping
  • 必须明确启用实时成型。为此,需要在ListCollectionBindingListCollectionView对象上设置多个属性

  最后一点最重要。实时成型添加了额外开销,因此时候启用要酌情而定。,为此,需要使用三个独立属性:IsLiveFilteringIsLiveSortingIsLiveGrouping。其中的每个属性为不同的视图功能启用实时成型。 例如,如果将IsLiveFiltering设置为true,当未设置其他两个属性,集合将检查那些影响当前设置的过滤条件的变化,但会忽略那些影响列表的排序和分组的变化

  启用了实时成型后,还需要告知集合需要监视哪些属性。为此,为三个集合属性中的其中一个添加字符串形式的属性名:LiveFilteringPropertiesLiveSortingPropertiesLiveGroupingProperties。与上面一样,此设计旨在确保获得最佳性能,忽略那些不重要的属性。

例如,考虑价格过滤产品示例。在这种情形中,合理的做法时启用IsLiveFiltering并监视Product.UnitCost属性的变化,因为这是能够影响列表过滤的唯一属性。对诸如DescriptionModelNumber的其他属性的公开不会影响产品是否被过滤,因此并不重要。为在此例中使用实时成型,可添加如下代码:

1
2
3
4
ListCollectionView view = CollectionViewSource.GetDefaultView(_products) as ListCollectionView;

view.IsLiveFiltering = true;
view.LiveFilteringProperties.Add("UnitCost");

  现在尝试编辑记录,并将价格降至过滤条件以下。Product对象会报告此更改,ListCollectionView会注意到这一点,成型评估条件,然后刷新视图。最终结果时,低价记录自动消失。