带搜索过滤器的 WPF ListView 自定义控件教程






4.28/5 (6投票s)
本教程介绍如何创建一个FilteredListView:一个具有搜索过滤功能的ListView自定义控件,它使用了节流(Throttling)技术。
引言
WPF中一个常见的挑战是创建一个带有搜索控件的ListView
,该控件可以过滤其项目。本教程将向您展示如何创建一个派生自ListView
的自定义控件FilteredListView
,以实现过滤功能。
本文所述的实现具有一些其他实现所没有的重要优点:
- 搜索控件是
ListView
模板的一部分,这使得它的使用尽可能简单。 - 过滤在您键入文本时立即进行,无需点击按钮(尽管,也很容易将代码更改为点击时过滤)。
- 过滤使用
<string>
响应式扩展(Rx)的节流(Throttle)来实现最佳用户体验和性能。这意味着,当用户停止输入半秒钟后,才会执行过滤。
Using the Code
最简单的用法是:
<FilteredListView ItemsSource={Binding Items}/>
如您所见,Filter
不需要额外的TextBox
。原因是TextBox
已包含在自定义控件的模板中。
默认的过滤是通过调用项目的.ToString()
方法来实现的。我们可以自定义它并构建自己的过滤逻辑。例如,当我们的项目是Person
类型时,我们可以定义Filter Predicate
来同时检查Name
和Occupation
属性。
<FilteredListView ItemsSource={Binding Items} FilterPredicate="{Binding MyFilter}"/>
在ViewModel
中
public Func<object, string, bool> MyFilter
{
get
{
return (item, text) =>
{
var person = item as Person;
return person.Name.Contains(text)
|| person.Occupation.Contains(text);
};
}
}
创建FilteredListView教程
自定义控件应该用于扩展现有功能,但同时保持现有功能的使用。我们的场景就是一个典型的例子。我们想给ListView
添加一个**搜索过滤器**,同时保留ListView
的所有现有属性和方法。
首先要做的是创建一个自定义控件。在Visual Studio中,最好通过**项目 | 添加新项**来完成,然后搜索**自定义控件(WPF)**。这将创建一个派生自Control
的类,并在*Themes\Generic.xaml*文件中创建一个默认模板。
在接下来的教程中,我假设您具有WPF和MVVM的基础知识。如果您是**自定义控件**和**默认样式**的新手(或者想更好地理解它们),我建议阅读WPF中的显式、隐式和默认样式。
创建默认样式
创建自定义控件时,最好从WPF现有的默认样式开始,然后在此基础上进行修改。使用Blend可以轻松做到这一点。从Blend获取原始样式后,将其复制粘贴到*Generic.xaml*并开始编辑。在这种情况下,我只需要在所有内容之上添加一个用于Filter
的TextBox
。
Generic.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FilteredListViewControl">
<FontFamily x:Key="FontAwesome">pack://application:,,,
/FilteredListViewControl;component/fonts/fontawesome-webfont.ttf#FontAwesome</FontFamily>
<SolidColorBrush x:Key="ListBox.Static.Background" Color="Transparent"/>
<SolidColorBrush x:Key="ListBox.Static.Border" Color="Transparent"/>
<SolidColorBrush x:Key="ListBox.Disabled.Background" Color="#FFFFFFFF"/>
<SolidColorBrush x:Key="ListBox.Disabled.Border" Color="#FFD9D9D9"/>
<Style TargetType="{x:Type local:FilteredListView}" >
<Setter Property="Background" Value="{StaticResource ListBox.Static.Background}"/>
<Setter Property="BorderBrush" Value="{StaticResource ListBox.Static.Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FilteredListView}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Padding="0 5">
<Grid>
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock FontFamily="{StaticResource FontAwesome}"
Text="" FontSize="14"
VerticalAlignment="Center"
HorizontalAlignment="Right"/>
</Grid>
</Border>
<Border Grid.Row="1" x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" Padding="1" SnapsToDevicePixels="true">
<ScrollViewer Focusable="false" Padding="{TemplateBinding Padding}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="Bd"
Value="{StaticResource ListBox.Disabled.Background}"/>
<Setter Property="BorderBrush" TargetName="Bd"
Value="{StaticResource ListBox.Disabled.Border}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true"/>
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
这可能看起来很多,但我实际上只需要添加一个TextBox
和一个来自Font Awesome的小放大镜图标。
<Border Padding="0 5">
<Grid>
<TextBox Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged,
RelativeSource={RelativeSource TemplatedParent}}"/>
<TextBlock FontFamily="{StaticResource FontAwesome}"
Text="" FontSize="14" Margin="0 0"
VerticalAlignment="Center"
HorizontalAlignment="Right"/>
</Grid>
</Border>
关于此代码的一些说明
- 我在这里使用了
FontAwesome
来显示右侧的小放大镜图标。这可以通过NuGet包FontAwesome获得。 TextBox
的文本绑定到FilterText
,并且该绑定设置为PropertyChanged
触发模式。这意味着更改回调会在每次击键时被调用,而不是默认行为(在失去焦点时调用)。
自定义控件代码
整个过滤代码如下放置在我们的自定义控件类中:
public class FilteredListView : ListView
{
static FilteredListView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FilteredListView),
new FrameworkPropertyMetadata(typeof(FilteredListView)));
}
public Func<object, string, bool> FilterPredicate
{
get { return (Func<object, string, bool>)GetValue(FilterPredicateProperty); }
set { SetValue(FilterPredicateProperty, value); }
}
public static readonly DependencyProperty FilterPredicateProperty =
DependencyProperty.Register("FilterPredicate",
typeof(Func<object, string, bool>), typeof(FilteredListView), new PropertyMetadata(null));
public Subject<bool> FilterInputSubject = new Subject<bool>();
public string FilterText
{
get { return (string)GetValue(FilterTextProperty); }
set { SetValue(FilterTextProperty, value); }
}
public static readonly DependencyProperty FilterTextProperty =
DependencyProperty.Register("FilterText",
typeof(string),
typeof(FilteredListView),
new PropertyMetadata("",
//This is the 'PropertyChanged' callback that's called
//whenever the Filter input text is changed
(d, e) => (d as FilteredListView).FilterInputSubject.OnNext(true)));
public FilteredListView()
{
SetDefaultFilterPredicate();
InitThrottle();
}
private void SetDefaultFilterPredicate()
{
FilterPredicate = (obj, text) => obj.ToString().ToLower().Contains(text);
}
private void InitThrottle()
{
FilterInputSubject.Throttle(TimeSpan.FromMilliseconds(500))
.ObserveOnDispatcher()
.Subscribe(HandleFilterThrottle);
}
private void HandleFilterThrottle(bool b)
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.ItemsSource);
if (collectionView == null) return;
collectionView.Filter = (item) => FilterPredicate(item, FilterText);
}
}
让我们解释一下这里写的内容。
- 自定义控件类派生自
ListView
。这将继承ListView
的全部行为,并允许我们对其进行扩展,这也是自定义控件的意义所在。 static
构造函数是任何自定义控件的样板代码,它告诉WPF使用您的默认样式。FilterPredicate
依赖属性是我们过滤的自定义表达式,可以从外部设置。默认实现只是调用项目的.ToString()
并检查文本是否包含FilterText
。FilterText
是将我们的TextBox
文本绑定的属性。每次在TextBox
中输入更改时,我们都会调用FilterInputSubject.OnNext(true)
,这会触发节流机制。在没有调用500毫秒后,节流就会被执行。SetDefaultFilterPredicate
设置如上所述的默认FilterPredicate
。InitThrottle
初始化**节流**,使其在没有动作的情况下延迟500毫秒后触发,然后调用HandleFilterThrottle
。
使用<string>
响应式扩展需要NuGet包:System.Reactive.Linq和System.Reactive。HandleFilterThrottle
重新应用Filter
到我们的ListView
。由于FilterText
可能已更改,因此有必要再次设置Filter
属性。
摘要
本教程到此结束。希望您都理解了并且从中受益。
知道何时使用**自定义控件**或**用户控件**可能令人困惑。您可以将用户控件视为一个可重用的UI组件,它不扩展任何先前的控件。而自定义控件则为现有控件添加功能。派生自Control
的自定义控件意义不大,因为它没有现有的功能。派生自ListBox
、Button
或StackPanel
则是有意义的。
自定义控件是WPF中的强大工具。我认为它们比用户控件最初需要更多的工作,但一旦准备好并在其他控件中使用它们会更加方便。这使得它们非常适合您解决方案中的专用**控件**类库。