65.9K
CodeProject 正在变化。 阅读更多。
Home

开发自动筛选列表视图

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (25投票s)

2008 年 7 月 8 日

CPOL

7分钟阅读

viewsIcon

164785

downloadIcon

5390

本文介绍了如何开发一个类似 Excel 的自动筛选 ListView。

filterablelistviewstyled.jpg

引言

Microsoft Excel 有一个非常有用的自动筛选功能。启用此功能后,用户可以从下拉列表中选择筛选器,选择后筛选器将应用于该列。本文介绍了如何将类似的筛选功能添加到 WPF ListView 控件。

背景

在 WPF 出现之前,控件自定义是一项棘手且有些混乱的任务。对于 WinForms,如果控件公开了允许开发人员在渲染之前调整某些样式方面的事件,则可以进行少量自定义。对于其他控件,可以重写 Paint 方法以在控件自身渲染后执行额外的渲染。然而,控件外观的任何重大更改都可能需要开发人员负责渲染整个控件(所有者绘制)。总而言之,使用 WinForms 进行控件自定义感觉就像是在黑客攻击!

使用 WPF,情况要好得多,控件用于渲染自身的机制对开发人员来说更加可见。使用各种模板(控件和数据)允许开发人员轻松地进行任何操作,从对控件外观的微小调整到彻底的视觉改造。WPF 和 WinForms 之间的重要区别在于,WPF 的控件渲染过程是立即可访问的;而 WinForms,它是隐藏的。

第一步:从何开始

我希望这个控件不仅提供筛选功能,还允许用户通过单击列标题进行排序。为了避免重复发明轮子,我在 Joel Rumerman 的博客 上找到了一个可排序的 ListView,它为我的控件提供了一个合适的起点。

下图显示了此控件的运行情况,列表按名字排序

sortablelistview.jpg

我创建了 SortableListView 的子类 FilterableListView,作为此控件的起点。

第二步:添加控件

不幸的是,本文的背景部分描绘了一幅美好的 WPF 图景,但这并不完全真实。有些控件比其他控件更难自定义,其中 ListView 就是比较棘手的控件之一。

ListView 控件的主要问题是,虽然插入此控件的 XAML 非常简单,如下图所示,但它实际上隐藏了相当多的复杂性。如果您使用像 Mole 这样的调试工具浏览 ListView 的视觉树,您会发现 ListView 由许多视觉元素组成,其中许多您都希望进行样式设置。

<ListView> 
    <ListView.View>
        <GridView>
            <GridViewColumn Header=”Column Header“/> 
        </GridView>
    </ListView.View>
</ListView>

有许多精彩的博客文章描述了如何设置 ListView 的各种视觉方面的样式,包括 设置 ListView 样式,其中描述了如何设置包含列表中项目的 ItemContainer(正如您可能期望的!)、每个 GridViewColumnCellTemplateColumnHeaderContainerStyle 等的样式。基本原则是 ListView 将其构建的视觉元素的模板和样式作为 ListView 本身​​的属性公开。困难的部分是知道使用这些属性中的哪一个才能达到您想要的效果。

回到创建自动筛选器的主题,下图显示了我想要添加的控件。一个按钮,单击它时会显示一个弹出窗口,其中包含要筛选的项目列表。

filtercloseup.jpg

为了实现上述目标,我们需要修改的是列标题的内容,而不是整个标题的视觉样式。因此,列标题的数据模板是添加这些控件的正确位置。下面的 XAML 说明了如何完成此操作。

<DataTemplate x:Key="FilterGridHeaderTemplate">
    <StackPanel Orientation="Horizontal">
        <!-- render the header text -->
        <TextBlock HorizontalAlignment="Center" 
          VerticalAlignment="Center" Text="{Binding}"/>
        <!-- add a label which is used to display the sort indicator -->
        <Label Name="sortIndicator" 
          VerticalAlignment="Center" 
          Style="{StaticResource HeaderTemplateTransparent}"/>            
        <!-- Add a filter button and popup  -->
        <Button  ContentTemplate="{StaticResource filterButtonInactiveTemplate}"
                 Name="filterButton" 
                 Command="{x:Static c:FilterableListView.ShowFilter}"/>
        <Popup StaysOpen="false" 
               Name="filterPopup" Placement="Bottom"                    
               PlacementTarget="{Binding ElementName=filterButton}">
            <ListView x:Name="filterList" 
                   ItemsSource="{Binding}"  BorderThickness="1"                             
                   ItemContainerStyle="{StaticResource ListItemRolloverHighlight}">
               <ListView.View>
                    <GridView>
                        <!-- hide the column header -->
                        <GridView.ColumnHeaderContainerStyle>
                            <Style TargetType="GridViewColumnHeader">
                                <Setter Property="Visibility" Value="Hidden" />
                                <Setter Property="Height" Value="0" />
                            </Style>
                        </GridView.ColumnHeaderContainerStyle>
                        <GridViewColumn DisplayMemberBinding="{Binding Path=ItemView}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </Popup>
    </StackPanel>
</DataTemplate>

如果您阅读了上面引用的博客文章,您会知道每个 GridViewColumn 都有一个 HeaderTemplate 属性,可用于为列标题提供数据模板。我们可以简单地指定当开发人员使用 FilterableListView 时,他们会相应地为每列设置 HeaderTemplate;但是,如果上述模板自动成为列标题的数据模板,则该控件会更有用。

为此,我们以编程方式在 ListViewOnInitialised 方法中分配 HeaderTemplate 属性,如下所示

protected override void OnInitialized(EventArgs e)
{
    base.OnInitialized(e);
 
    Uri uri = new Uri("/Controls/FiterListViewDictionary.xaml", UriKind.Relative);
    dictionary = Application.LoadComponent(uri) as ResourceDictionary;
  
    // cast the ListView's View to a GridView 
    GridView gridView = this.View as GridView;
    if (gridView != null)
    {
        foreach (GridViewColumn gridViewColumn in gridView.Columns)
        {
            gridViewColumn.HeaderTemplate = 
               (DataTemplate)dictionary["FilterGridHeaderTemplate"];                    
        }
    }
}

第三步:显示弹出窗口

将组件放置在窗口中时,事件处理是连接事件到代码后面的处理程序的直接过程。但是,在这种情况下,我们的数据模板是在资源字典中指定的。起初可能看起来令人惊讶,但您无法直接将单击事件连接到代码!以下博客文章更详细地描述了此问题:WPF 中的命令和控件,解决方案是使用命令,使视图 (XAML) 和控制器 (代码后面) 之间的耦合更加松散。

在列表 2 中,您可以看到筛选按钮发出 ShowFilter 命令;这是一个将在视觉树中隧道和冒泡的 RoutedEventFilterableListView 有一个命令绑定,允许它处理此事件。

FilterableListView 处理 ShowFilter 命令时,它需要确定以下内容

  • 应筛选的属性名称。
  • 哪个弹出窗口与此按钮关联。

发现此信息最简单的方法是导航视觉树,首先向上到列标题,获取要筛选的属性名称,然后从标题向下查找关联的弹出窗口。VisualTreeHelper 类提供了许多用于导航树的静态实用方法,Andrew Whiddett 将它们封装在 WPFHelper 类中,该类增强了此功能,允许您搜索视觉树中匹配特定条件(名称、类型等)的类。

public FilterableListView()
{
    CommandBindings.Add(new CommandBinding(ShowFilter, ShowFilterCommand));            
}

private void ShowFilterCommand(object sender, ExecutedRoutedEventArgs e)
{
    Button button = e.OriginalSource as Button;
    
    // navigate up to the header 
    GridViewColumnHeader header = (GridViewColumnHeader)
        Helpers.FindElementOfTypeUp(button, typeof(GridViewColumnHeader));
  
    // then down to the popup 
    Popup popup = (Popup)Helpers.FindElementOfType(header, typeof(Popup));
  
    if (popup != null)
    {
        // find the property name that we are filtering 
        SortableGridViewColumn column = (SortableGridViewColumn)header.Column;
        String propertyName = column.SortPropertyName;
  
        // clear the previous filter 
        filterList.Clear();
  
        PropertyDescriptor filterPropDesc =
            TypeDescriptor.GetProperties(typeof(Employee))[propertyName];
  
        // iterate over all the objects in the list 
        foreach (Object item in Items)
        {
            object value = filterPropDesc.GetValue(employee);
            if (value != null)
            {
                FilterItem filterItem = new FilterItem(value as IComparable);
                if(!filterList.Contains(filterItem))
                {
                    filterList.Add(filterItem);
                }
            }
        }
 
        filterList.Sort();
 
 
        // open the popup to display this list 
        popup.DataContext = filterList;
        popup.IsOpen = true;
  
        // connect to the selection change event 
        ListView listView = (ListView)popup.Child;
        listView.SelectionChanged += SelectionChangedHandler;
    }
}

第四步:应用筛选器

最后一步是简单地处理筛选 ListViewSelectionChange 事件,构造适当的筛选器并将其应用于列表中的 Item。当使用 FilterableList 视图,并且其中一列设置了筛选器,您希望对其他列应用筛选器时,您会期望下拉列表只显示由于第一个筛选器而存在的项目。换句话说,筛选器是 AND 组合的。有趣的是,此功能是免费提供的!这是因为我们通过迭代 ListViewItems 属性填充了下拉筛选列表。Items 派生自 System.Windows.Data.CollectionView,顾名思义,它是绑定数据的视图,是应用分组、排序、筛选等的效果。

// construct a filter and apply it               
Items.Filter = delegate(object item)
{
    // when applying the filter to each item, iterate over all of 
    // the current filters 
    bool match = true;
    foreach (KeyValuePair<String, FilterStruct> filter in currentFilters)
    {
        FilterStruct filter = filter.value;
  
        // obtain the value for this property on the item under test 
        PropertyDescriptor filterPropDesc =
            TypeDescriptor.GetProperties(typeof(Employee))[filter.property];
        object itemValue = filterPropDesc.GetValue((Employee)item);
  
        if (itemValue != null)
        {
            // check to see if it meets our filter criteria 
            if (!itemValue.Equals(filter.value.Item))
                match = false;
        }
        else 
        {
            if (filter.value.Item != null)
                match = false;
        }
    }
    return match;
};

实际使用控件

使用 FilterableListView 和使用常规 ListView 一样简单。唯一的区别是增加了一个额外的属性 SortPropertyName,它是用于特定列的排序/筛选属性。

<slogic:FilterableListView ItemsSource="{Binding}">
    <ListView.View>
        <GridView>
            <slogic:SortableGridViewColumn Header="Surname"
               SortPropertyName="LastName" 
               DisplayMemberBinding="{Binding Path=LastName}"/>
            <slogic:SortableGridViewColumn Header="Forename"
               SortPropertyName="FirstName" 
               DisplayMemberBinding="{Binding Path=FirstName}" />
            <slogic:SortableGridViewColumn Header="Salary"
               SortPropertyName="Salary" 
               DisplayMemberBinding="{Binding Path=Salary}" />
            <slogic:SortableGridViewColumn Header="Start Date"
               SortPropertyName="StartDate" 
               DisplayMemberBinding="{Binding Path=StartDate}" />
        </GridView>
    </ListView.View>
</slogic:FilterableListView>

但是,FilterableListViewListView 存在相同的问题。该控件由许多其他控件组成,开发人员在他们的 XAML 代码中没有明确构造这些控件。FilterableListView 使用与 ListView 相同的方法;它通过依赖属性向开发人员公开其包含的视觉元素的属性。FilterButtonActiveFilterButtonInactive 属性可用于设置下拉按钮的样式。(注意:我没有公开所有您可能感兴趣的潜在属性,我将这留作读者的练习!)

下图显示了 FilterableListView 的几个实际示例。一个使用其默认样式,另一个对列标题、按钮和 ListView 项容器应用了样式。

filterablelistview.jpg

filterablelistviewstyled.jpg

结论

本文演示了如何增强提供的 ListView 控件的功能,以向列标题添加自动筛选器。WPF ListView 控件起初感觉有点难以访问;但是,它并不比 WPF 库中的其他控件更难自定义。

历史

  • 2008 年 7 月 8 日 - 初始文章上传。
© . All rights reserved.