开发自动筛选列表视图






4.97/5 (25投票s)
本文介绍了如何开发一个类似 Excel 的自动筛选 ListView。
引言
Microsoft Excel 有一个非常有用的自动筛选功能。启用此功能后,用户可以从下拉列表中选择筛选器,选择后筛选器将应用于该列。本文介绍了如何将类似的筛选功能添加到 WPF ListView
控件。
背景
在 WPF 出现之前,控件自定义是一项棘手且有些混乱的任务。对于 WinForms,如果控件公开了允许开发人员在渲染之前调整某些样式方面的事件,则可以进行少量自定义。对于其他控件,可以重写 Paint
方法以在控件自身渲染后执行额外的渲染。然而,控件外观的任何重大更改都可能需要开发人员负责渲染整个控件(所有者绘制)。总而言之,使用 WinForms 进行控件自定义感觉就像是在黑客攻击!
使用 WPF,情况要好得多,控件用于渲染自身的机制对开发人员来说更加可见。使用各种模板(控件和数据)允许开发人员轻松地进行任何操作,从对控件外观的微小调整到彻底的视觉改造。WPF 和 WinForms 之间的重要区别在于,WPF 的控件渲染过程是立即可访问的;而 WinForms,它是隐藏的。
第一步:从何开始
我希望这个控件不仅提供筛选功能,还允许用户通过单击列标题进行排序。为了避免重复发明轮子,我在 Joel Rumerman 的博客 上找到了一个可排序的 ListView,它为我的控件提供了一个合适的起点。
下图显示了此控件的运行情况,列表按名字排序
我创建了 SortableListView
的子类 FilterableListView
,作为此控件的起点。
第二步:添加控件
不幸的是,本文的背景部分描绘了一幅美好的 WPF 图景,但这并不完全真实。有些控件比其他控件更难自定义,其中 ListView
就是比较棘手的控件之一。
ListView
控件的主要问题是,虽然插入此控件的 XAML 非常简单,如下图所示,但它实际上隐藏了相当多的复杂性。如果您使用像 Mole 这样的调试工具浏览 ListView
的视觉树,您会发现 ListView
由许多视觉元素组成,其中许多您都希望进行样式设置。
<ListView>
<ListView.View>
<GridView>
<GridViewColumn Header=â€Column Header“/>
</GridView>
</ListView.View>
</ListView>
有许多精彩的博客文章描述了如何设置 ListView
的各种视觉方面的样式,包括 设置 ListView 样式,其中描述了如何设置包含列表中项目的 ItemContainer
(正如您可能期望的!)、每个 GridViewColumn
的 CellTemplate
、ColumnHeaderContainerStyle
等的样式。基本原则是 ListView
将其构建的视觉元素的模板和样式作为 ListView
本身的属性公开。困难的部分是知道使用这些属性中的哪一个才能达到您想要的效果。
回到创建自动筛选器的主题,下图显示了我想要添加的控件。一个按钮,单击它时会显示一个弹出窗口,其中包含要筛选的项目列表。
为了实现上述目标,我们需要修改的是列标题的内容,而不是整个标题的视觉样式。因此,列标题的数据模板是添加这些控件的正确位置。下面的 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
;但是,如果上述模板自动成为列标题的数据模板,则该控件会更有用。
为此,我们以编程方式在 ListView
的 OnInitialised
方法中分配 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
命令;这是一个将在视觉树中隧道和冒泡的 RoutedEvent
。FilterableListView
有一个命令绑定,允许它处理此事件。
当 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;
}
}
第四步:应用筛选器
最后一步是简单地处理筛选 ListView
的 SelectionChange
事件,构造适当的筛选器并将其应用于列表中的 Item
。当使用 FilterableList
视图,并且其中一列设置了筛选器,您希望对其他列应用筛选器时,您会期望下拉列表只显示由于第一个筛选器而存在的项目。换句话说,筛选器是 AND 组合的。有趣的是,此功能是免费提供的!这是因为我们通过迭代 ListView
的 Items
属性填充了下拉筛选列表。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>
但是,FilterableListView
和 ListView
存在相同的问题。该控件由许多其他控件组成,开发人员在他们的 XAML 代码中没有明确构造这些控件。FilterableListView
使用与 ListView
相同的方法;它通过依赖属性向开发人员公开其包含的视觉元素的属性。FilterButtonActive
和 FilterButtonInactive
属性可用于设置下拉按钮的样式。(注意:我没有公开所有您可能感兴趣的潜在属性,我将这留作读者的练习!)
下图显示了 FilterableListView
的几个实际示例。一个使用其默认样式,另一个对列标题、按钮和 ListView 项容器应用了样式。
结论
本文演示了如何增强提供的 ListView
控件的功能,以向列标题添加自动筛选器。WPF ListView
控件起初感觉有点难以访问;但是,它并不比 WPF 库中的其他控件更难自定义。
历史
- 2008 年 7 月 8 日 - 初始文章上传。