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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.28/5 (6投票s)

2018 年 11 月 29 日

MIT

4分钟阅读

viewsIcon

32082

downloadIcon

1546

本教程介绍如何创建一个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来同时检查NameOccupation属性。

<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*并开始编辑。在这种情况下,我只需要在所有内容之上添加一个用于FilterTextBox

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="&#xf002;" 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="&#xf002;" 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.LinqSystem.Reactive
  • HandleFilterThrottle重新应用Filter到我们的ListView。由于FilterText可能已更改,因此有必要再次设置Filter属性。

摘要

本教程到此结束。希望您都理解了并且从中受益。

知道何时使用**自定义控件**或**用户控件**可能令人困惑。您可以将用户控件视为一个可重用的UI组件,它不扩展任何先前的控件。而自定义控件则为现有控件添加功能。派生自Control的自定义控件意义不大,因为它没有现有的功能。派生自ListBoxButtonStackPanel则是有意义的。

自定义控件是WPF中的强大工具。我认为它们比用户控件最初需要更多的工作,但一旦准备好并在其他控件中使用它们会更加方便。这使得它们非常适合您解决方案中的专用**控件**类库。

© . All rights reserved.