在wpf中雖然ObservableCollection<T>作為ListBox的Itemsource,很好,很強大!但是CollectionViewSource與ListBox才是天作之合!
wpf中ListBox支持分組顯示,CollectionViewSource.GroupDescriptions為其實現了分組。廢話不多說,下面上ListBox分組顯示的Demo代碼:
XAML:

<Window x:Class="WpfListGroup.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" Title="MainWindow" Height="450" Width="525"> <Window.Resources> <CollectionViewSource x:Key="employeeCollectionViewSource" Filter="employeeCollectionViewSource_Filter"> <CollectionViewSource.SortDescriptions> <!--排序描述--> <scm:SortDescription PropertyName="Num"/> </CollectionViewSource.SortDescriptions> <CollectionViewSource.GroupDescriptions> <!--分組描述--> <PropertyGroupDescription PropertyName="Title"/> </CollectionViewSource.GroupDescriptions> </CollectionViewSource> <Style x:Key="ButtonFocusVisual"> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate> <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/> </ControlTemplate> </Setter.Value> </Setter> </Style> <LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0"> <GradientStop Color="#F3F3F3" Offset="0"/> <GradientStop Color="#EBEBEB" Offset="0.5"/> <GradientStop Color="#DDDDDD" Offset="0.5"/> <GradientStop Color="#CDCDCD" Offset="1"/> </LinearGradientBrush> <SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/> <Style x:Key="nocheckedButtonStyle" TargetType="{x:Type Button}"> <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/> <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/> <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/> <Setter Property="BorderThickness" Value="1"/> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/> <Setter Property="HorizontalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Padding" Value="1"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Grid Width="29.72"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[2].(RotateTransform.Angle)" Storyboard.TargetName="contentPresenter"> <EasingDoubleKeyFrame KeyTime="0" Value="0"/> <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="90"/> </DoubleAnimationUsingKeyFrames> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)" Storyboard.TargetName="ellipse"> <EasingColorKeyFrame KeyTime="0" Value="#FF2CA50B"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Pressed"/> <VisualState x:Name="Disabled"/> </VisualStateGroup> <VisualStateGroup x:Name="FocusStates"> <VisualState x:Name="Unfocused"/> <VisualState x:Name="Focused"/> </VisualStateGroup> <VisualStateGroup x:Name="ValidationStates"> <VisualState x:Name="Valid"/> <VisualState x:Name="InvalidFocused"/> <VisualState x:Name="InvalidUnfocused"/> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <Ellipse x:Name="ellipse" Fill="#FF75AB80" Margin="0" Stroke="{x:Null}" VerticalAlignment="Stretch" Width="16" Height="16"/> <Microsoft_Windows_Themes:ButtonChrome x:Name="Chrome" SnapsToDevicePixels="true" > <ContentPresenter x:Name="contentPresenter" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RenderTransformOrigin="0.5,0.5"> <ContentPresenter.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform/> </TransformGroup> </ContentPresenter.RenderTransform> </ContentPresenter> </Microsoft_Windows_Themes:ButtonChrome> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"/> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="25"/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="150"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <DockPanel Grid.Row="0" LastChildFill="True" > <TextBlock VerticalAlignment="Center" DockPanel.Dock="Left" Text="搜索:"/> <Button Content=" × " VerticalAlignment="Center" DockPanel.Dock="Right" Background="White" BorderBrush="{x:Null}" Margin="0" Style="{DynamicResource nocheckedButtonStyle}" HorizontalAlignment="Right" FontFamily="Forte" Foreground="White" ToolTip="清空" Click="btnClearKeyword_Click"/> <TextBox x:Name="txtEmployeeKeyword" VerticalAlignment="Center" TextChanged="txtEmployeeKeyword_TextChanged" /> </DockPanel> <ScrollViewer x:Name="scv1" Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <ListBox x:Name="lbx1" SelectionMode="Extended" ItemsSource="{Binding Source={StaticResource ResourceKey=employeeCollectionViewSource}}"> <!--分組樣式--> <ListBox.GroupStyle> <GroupStyle> <GroupStyle.ContainerStyle> <Style TargetType="{x:Type GroupItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type GroupItem}"> <Expander> <Expander.Header> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Margin="0,0,10,0"> <!--分組的組名--> <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" /> <!--該分組元素(員工)的總和數--> <TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount, StringFormat=(共{0}條)}"/> </StackPanel> <Line Grid.Column="1" SnapsToDevicePixels="true" X1="0" X2="1" Stretch="Fill" StrokeThickness="1"/> </Grid> </Expander.Header> <ItemsPresenter /> </Expander> </ControlTemplate> </Setter.Value> </Setter> </Style> </GroupStyle.ContainerStyle> </GroupStyle> </ListBox.GroupStyle> <!--右鍵菜單--> <ListBox.ContextMenu> <ContextMenu> <MenuItem Header="Show" Click="MenuItem_Click"/> </ContextMenu> </ListBox.ContextMenu> <!--“沒有”綁定ListBox.ItemTemplate,是因為在Employee類重寫了ToString()方法--> </ListBox> </ScrollViewer> <ScrollViewer x:Name="scv2" Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Visibility="Collapsed"> <ListBox Name="lbx2" ItemsSource="{Binding Source={StaticResource employeeCollectionViewSource}}" SelectionMode="Extended"> <!--按Ctrl鍵可多選--> <ListBox.ContextMenu> <ContextMenu> <!--右鍵菜單--> <MenuItem Header="Show" Click="MenuItem_Click"/> </ContextMenu> </ListBox.ContextMenu> </ListBox> </ScrollViewer> </Grid> </Window>
C#:

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.ComponentModel; using System.Collections.ObjectModel; namespace WpfListGroup { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); #region 基礎數據(員工集合) ObservableCollection<Employee> employeeList = new ObservableCollection<Employee> { new Employee{EmployeeNum="0027",EmployeeName="張三",Sex="男",Title="副經理"}, new Employee{EmployeeNum="1086",EmployeeName="春麗",Sex="女",Title="秘書"}, new Employee{EmployeeNum="1031",EmployeeName="王五",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1211",EmployeeName="趙陽",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1201",EmployeeName="孫迪",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1416",EmployeeName="李玥玥",Sex="女",Title="秘書"}, new Employee{EmployeeNum="0017",EmployeeName="錢哆哆",Sex="男",Title="副經理"}, new Employee{EmployeeNum="1016",EmployeeName="周暢",Sex="女",Title="秘書"}, new Employee{EmployeeNum="1231",EmployeeName="鄭超",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1131",EmployeeName="王思聰",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1871",EmployeeName="李文",Sex="男",Title="普通員工"}, new Employee{EmployeeNum="1266",EmployeeName="周琪妹",Sex="女",Title="秘書"} }; #endregion CollectionViewSource employeeCvs = (CollectionViewSource)this.FindResource("employeeCollectionViewSource"); employeeCvs.Source = employeeList; } /// <summary> /// 右鍵菜單、按住Ctrl鍵可多選 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void MenuItem_Click(object sender, RoutedEventArgs e) { //獲取關鍵字 string keyword = txtEmployeeKeyword.Text.Trim(); if (string.IsNullOrEmpty(keyword))//如果沒有關鍵字 { if (lbx1.SelectedItem != null)//判斷lbx1有沒有選中項 { foreach (var item in lbx1.SelectedItems) { Employee employee = item as Employee; string msg = string.Format("姓名:{0},工號:{1},性別:{2},職位:{3}", employee.EmployeeName, employee.EmployeeNum, employee.Sex, employee.Title); MessageBox.Show(msg); } } } else { if (lbx2.SelectedItem != null)//有關鍵字的話,顯示lbx2 { foreach (var item in lbx2.SelectedItems)//判斷lbx2有沒有選中項 { Employee employee = item as Employee; string msg = string.Format("姓名:{0},工號:{1},性別:{2},職位:{3}", employee.EmployeeName, employee.EmployeeNum, employee.Sex, employee.Title); MessageBox.Show(msg); } } } } /// <summary> /// 關鍵字改變時觸發 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void txtEmployeeKeyword_TextChanged(object sender, TextChangedEventArgs e) { string keyword = txtEmployeeKeyword.Text.Trim(); if (string.IsNullOrEmpty(keyword))//無關鍵字,顯示scv1下的listbox(有分組) { scv1.Visibility = Visibility.Visible; scv2.Visibility = Visibility.Collapsed; } else//有關鍵字,顯示scv2下的listbox(無分組) { scv1.Visibility = Visibility.Collapsed; scv2.Visibility = Visibility.Visible; } CollectionViewSource employeeCvs = (CollectionViewSource)this.FindResource("employeeCollectionViewSource"); employeeCvs.View.Refresh();//刷新View } /// <summary> /// 根據關鍵字(工號或姓名)篩選員工 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void employeeCollectionViewSource_Filter(object sender, FilterEventArgs e) { string keyword = txtEmployeeKeyword.Text.Trim(); Employee employee = e.Item as Employee; if (employee != null) { if (string.IsNullOrEmpty(keyword))//無關鍵字,直接Accept { e.Accepted = true; } else { //有關鍵字、篩選員工號或姓名中包含關鍵字的員工 e.Accepted = employee.EmployeeNum.Contains(keyword) || employee.EmployeeName.Contains(keyword); } } } /// <summary> /// 清空關鍵字 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnClearKeyword_Click(object sender, RoutedEventArgs e) { this.txtEmployeeKeyword.Clear(); } } public class Employee:INotifyPropertyChanged { #region 實現更改通知 public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } #endregion /// <summary> /// 重載ToString()方法 /// </summary> /// <returns></returns> public override string ToString() { return this.EmployeeNum + " " + this.EmployeeName; } private string title; /// <summary> /// 職位 /// </summary> public string Title { get { return title; } set { title = value; RaisePropertyChanged("Title"); } } private string employeeName; /// <summary> /// 姓名 /// </summary> public string EmployeeName { get { return employeeName; } set { employeeName = value; RaisePropertyChanged("EmployeeName"); } } private string employeeNum; /// <summary> /// 工號 /// </summary> public string EmployeeNum { get { return employeeNum; } set { employeeNum = value; RaisePropertyChanged("EmployeeNum"); } } private string sex; /// <summary> /// 性別 /// </summary> public string Sex { get { return sex; } set { sex = value; RaisePropertyChanged("Sex"); } } } }
運行效果:
右鍵菜單點擊“Show” 彈出選中項的員工信息:
輸入關鍵字"同步"篩選模糊查詢員工:
點擊清空按鈕清空關鍵字,“恢復”分組數據:
總結核心xaml:
①資源CollectionViewSource, CollectionViewSource.GroupDescriptions:分組描述(依據),CollectionViewSource.SortDescriptions:分組排序(描述)
在資源中:

<CollectionViewSource x:Key="employeeCollectionViewSource" Filter="employeeCollectionViewSource_Filter"> <CollectionViewSource.SortDescriptions> <!--排序描述--> <scm:SortDescription PropertyName="Num"/> </CollectionViewSource.SortDescriptions> <CollectionViewSource.GroupDescriptions> <!--分組描述--> <PropertyGroupDescription PropertyName="Title"/> </CollectionViewSource.GroupDescriptions> </CollectionViewSource>
②綁定到ListBox的Itemsource上,設置分組樣式,使用Expander控件使分組可以折疊:

<ScrollViewer x:Name="scv1" Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <ListBox x:Name="lbx1" SelectionMode="Extended" ItemsSource="{Binding Source={StaticResource ResourceKey=employeeCollectionViewSource}}"> <!--分組樣式--> <ListBox.GroupStyle> <GroupStyle> <GroupStyle.ContainerStyle> <Style TargetType="{x:Type GroupItem}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type GroupItem}"> <Expander> <Expander.Header> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Margin="0,0,10,0"> <!--分組的組名--> <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" /> <!--該分組元素(員工)的總和數--> <TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount, StringFormat=(共{0}條)}"/> </StackPanel> <Line Grid.Column="1" SnapsToDevicePixels="true" X1="0" X2="1" Stretch="Fill" StrokeThickness="1"/> </Grid> </Expander.Header> <ItemsPresenter /> </Expander> </ControlTemplate> </Setter.Value> </Setter> </Style> </GroupStyle.ContainerStyle> </GroupStyle> </ListBox.GroupStyle> <!--右鍵菜單--> <ListBox.ContextMenu> <ContextMenu> <MenuItem Header="Show" Click="MenuItem_Click"/> </ContextMenu> </ListBox.ContextMenu> <!--“沒有”綁定ListBox.ItemTemplate,是因為在Employee類重寫了ToString()方法--> </ListBox> </ScrollViewer>
總結核心C#:
①CollectionViewSource的篩選器Filter的方法:

/// <summary> /// 根據關鍵字(工號或姓名)篩選員工 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void employeeCollectionViewSource_Filter(object sender, FilterEventArgs e) { string keyword = txtEmployeeKeyword.Text.Trim(); Employee employee = e.Item as Employee; if (employee != null) { if (string.IsNullOrEmpty(keyword))//無關鍵字,直接Accept { e.Accepted = true; } else { //有關鍵字、篩選員工號或姓名中包含關鍵字的員工 e.Accepted = employee.EmployeeNum.Contains(keyword) || employee.EmployeeName.Contains(keyword); } } }
②關鍵字文本框的文本發生改變時觸發的事件:

/// <summary> /// 關鍵字改變時觸發 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void txtEmployeeKeyword_TextChanged(object sender, TextChangedEventArgs e) { string keyword = txtEmployeeKeyword.Text.Trim(); if (string.IsNullOrEmpty(keyword))//無關鍵字,顯示scv1下的listbox(有分組) { scv1.Visibility = Visibility.Visible; scv2.Visibility = Visibility.Collapsed; } else//有關鍵字,顯示scv2下的listbox(無分組) { scv1.Visibility = Visibility.Collapsed; scv2.Visibility = Visibility.Visible; } CollectionViewSource employeeCvs = (CollectionViewSource)this.FindResource("employeeCollectionViewSource"); employeeCvs.View.Refresh();//刷新View }
總結:以上就是ListBox的分組、折疊、篩選顯示的Demo。日積月累,水滴石穿!