在CollectionViewSource上触发筛选器

54

我正在使用MVVM模式开发WPF桌面应用程序。

我试图基于在TextBox中输入的文本来过滤ListView中的一些项目。 我希望随着文本的更改,ListView项目会被过滤。

我想知道如何在过滤文本更改时触发过滤。

ListView绑定到CollectionViewSourceCollectionViewSource绑定到ViewModel上的ObservableCollection。 用于过滤文本的TextBox绑定到ViewModel上的一个字符串,并使用UpdateSourceTrigger=PropertyChanged

<CollectionViewSource x:Key="ProjectsCollection"
                      Source="{Binding Path=AllProjects}"
                      Filter="CollectionViewSource_Filter" />

<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />

<ListView DataContext="{StaticResource ProjectsCollection}"
          ItemsSource="{Binding}" />
Filter="CollectionViewSource_Filter"链接到后台代码中的事件处理程序,该处理程序简单地调用ViewModel上的过滤方法。

当FilterText的值更改时进行过滤-FilterText属性的Setter调用FilterList方法,该方法迭代ViewModel中的ObservableCollection,并在每个项目ViewModel上设置一个布尔FilteredOut属性。

我知道在过滤文本更改时更新了FilteredOut属性,但是列表没有刷新。只有在通过切换并返回UserControl时才会触发CollectionViewSource过滤事件。

我尝试在更新过滤信息后调用OnPropertyChanged("AllProjects"),但它没有解决我的问题。 ("AllProjects"是ViewModel上的ObservableCollection属性,CollectionViewSource绑定到该属性。)

如何在FilterText文本框的值更改时使CollectionViewSource重新过滤自身?

非常感谢


另外,是否有一种方法可以直接在我的ViewModel上调用过滤器方法(bool Include(object o)),这样我就不需要在代码后台中拥有事件处理程序了? - Pieter Müller
6个回答

80
不要在视图中创建CollectionViewSource。 相反,在视图模型中创建一个类型为ICollectionView的属性,并将ListView.ItemsSource绑定到它。完成这些操作后,您可以在FilterText属性的Setter中放置逻辑,每当用户更改该属性时调用ICollectionView上的Refresh()。
您会发现这也简化了排序问题:您可以将排序逻辑构建到视图模型中,然后公开视图可以使用的命令。
编辑:
这是一个非常简单的MVVM动态排序和过滤集合视图演示。此演示不实现FilterText,但一旦您理解了其工作原理,就不难实现使用该属性而不是现在正在使用的硬编码过滤器的谓词的FilterText属性。
(请注意,这里的视图模型类不实现属性更改通知。这只是为了保持代码简单:由于此演示中实际上没有更改属性值,因此不需要属性更改通知。)
首先是您的项目的一个类:
public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

现在,为应用程序创建一个视图模型。这里有三件事情要做:首先,它创建并填充自己的ICollectionView;第二,它公开一个ApplicationCommand(见下文),供视图使用以执行排序和过滤命令;最后,它实现一个Execute方法,用于对视图进行排序或过滤:

public class ApplicationViewModel
{
    public ApplicationViewModel()
    {
        Items.Add(new ItemViewModel { Name = "John", Age = 18} );
        Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
        Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
        Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
        Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
        Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });

        ItemsView = CollectionViewSource.GetDefaultView(Items);
    }

    public ApplicationCommand ApplicationCommand
    {
        get { return new ApplicationCommand(this); }
    }

    private ObservableCollection<ItemViewModel> Items = 
                                     new ObservableCollection<ItemViewModel>();

    public ICollectionView ItemsView { get; set; }

    public void ExecuteCommand(string command)
    {
        ListCollectionView list = (ListCollectionView) ItemsView;
        switch (command)
        {
            case "SortByName":
                list.CustomSort = new ItemSorter("Name") ;
                return;
            case "SortByAge":
                list.CustomSort = new ItemSorter("Age");
                return;
            case "ApplyFilter":
                list.Filter = new Predicate<object>(x => 
                                                  ((ItemViewModel)x).Age > 21);
                return;
            case "RemoveFilter":
                list.Filter = null;
                return;
            default:
                return;
        }
    }
}

排序有些糟糕;你需要实现一个IComparer

public class ItemSorter : IComparer
{
    private string PropertyName { get; set; }

    public ItemSorter(string propertyName)
    {
        PropertyName = propertyName;    
    }
    public int Compare(object x, object y)
    {
        ItemViewModel ix = (ItemViewModel) x;
        ItemViewModel iy = (ItemViewModel) y;

        switch(PropertyName)
        {
            case "Name":
                return string.Compare(ix.Name, iy.Name);
            case "Age":
                if (ix.Age > iy.Age) return 1;
                if (iy.Age > ix.Age) return -1;
                return 0;
            default:
                throw new InvalidOperationException("Cannot sort by " + 
                                                     PropertyName);
        }
    }
}

要触发视图模型中的Execute方法,需要使用一个ApplicationCommand类,它是ICommand的一个简单实现,将视图中按钮上的CommandParameter路由到视图模型的Execute方法。我采用这种方式实现是因为我不想在应用程序视图模型中创建一堆RelayCommand属性,并且我想将所有的排序/过滤都放在一个方法中,以便可以轻松地查看它是如何完成的。

public class ApplicationCommand : ICommand
{
    private ApplicationViewModel _ApplicationViewModel;

    public ApplicationCommand(ApplicationViewModel avm)
    {
        _ApplicationViewModel = avm;
    }

    public void Execute(object parameter)
    {
        _ApplicationViewModel.ExecuteCommand(parameter.ToString());
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;
}

最后,这是应用程序的MainWindow

<Window x:Class="CollectionViewDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo" 
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <CollectionViewDemo:ApplicationViewModel />
    </Window.DataContext>
    <DockPanel>
        <ListView ItemsSource="{Binding ItemsView}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Name}"
                                    Header="Name" />
                    <GridViewColumn DisplayMemberBinding="{Binding Age}" 
                                    Header="Age"/>
                </GridView>
            </ListView.View>
        </ListView>
        <StackPanel DockPanel.Dock="Right">
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByName">Sort by name</Button>
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByAge">Sort by age</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="ApplyFilter">Apply filter</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="RemoveFilter">Remove filter</Button>
        </StackPanel>
    </DockPanel>
</Window>

1
到目前为止,所有给出的答案都很好。我将选择这个作为最符合MVVM方式解决问题的答案,并且它还帮助我解决了一直以来存在的自定义排序问题。谢谢。 - Pieter Müller
罗伯特,你之前成功地实现过这个吗?我好像无法让它工作。 - Pieter Müller
我的实现都嵌入在太复杂而不能轻易抽离和发布的应用程序中,因此我为你准备了一个小型演示应用程序。请查看我的编辑。 - Robert Rossney
1
非常感谢您提供这个很好的例子。我一直在尝试通过构造函数来实例化CollectionView,而不是通过CollectionViewSource.GetDefaultView(),后者立即解决了我的问题。对我来说,这些东西似乎没有很好的文档记录。您帮了我大忙! :-) - Pieter Müller
非常好的答案,即使是在发布7年后(编辑2年后),它仍然具有相关性。感谢您的出色工作。 - dtoland
显示剩余2条评论

31

现在,通常不需要显式触发刷新。 CollectionViewSource 实现了 ICollectionViewLiveShaping 接口,如果 IsLiveFilteringRequested 为 true,则根据其 LiveFilteringProperties 集合中的字段自动更新。

XAML 中的示例:

  <CollectionViewSource
         Source="{Binding Items}"
         Filter="FilterPredicateFunction"
         IsLiveFilteringRequested="True">
    <CollectionViewSource.LiveFilteringProperties>
      <system:String>FilteredProperty1</system:String>
      <system:String>FilteredProperty2</system:String>
    </CollectionViewSource.LiveFilteringProperties>
  </CollectionViewSource>

7
我想说,这是在 .NET 4.5 中通过 WPF 添加的。 - Ahmed Fwela
4
这看起来有点短视。没有自定义绑定?如果您在视图中有一个项目集合,那么很可能会更改父视图模型上的某些值(例如筛选文本或布尔筛选标志),而不仅仅是在被过滤的集合中的项目属性。 - Trevor Elliott
在使用 .NET 4.6.1 的 WPF 中,ICollectionViewLiveShaping 没有被实现。 - 15ee8f99-57ff-4f92-890c-b56153
2
在我看来,这是正确的方式。被接受的答案需要在视图模型中依赖于演示框架,而我正在尝试将其与视图模型分离,以便稍后更轻松地切换UI层(例如到Uno框架)。 - James B
1
这是一个非常有用的答案。事实上,我简直不敢相信我花了这么长时间才发现ICollectionViewLiveShaping。我已经手动监控对象并手动更新过滤器太久了。真希望我4年前就知道这个。 - Joe
显示剩余4条评论

15
CollectionViewSource.View.Refresh();

CollectionViewSource.Filter 是通过这种方式重新评估的!


6

也许你在问题中简化了你的视图,但是按照写法,你实际上不需要一个CollectionViewSource - 你可以直接在你的ViewModel中绑定到一个过滤后的列表(mItemsToFilter是被过滤的集合,在你的例子中可能是"AllProjects"):

public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
    get 
    { 
        if (String.IsNullOrEmpty(mFilterText))
            return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);

        var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
        return new ReadOnlyObservableCollection<ItemsToFilter>(
            new ObservableCollection<ItemsToFilter>(filtered));
    }
}

public string FilterText
{
    get { return mFilterText; }
    set 
    { 
        mFilterText = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
            PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
        }
    }
}

你的视图将会是这样的:

<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />

一些快速笔记:

  • 这消除了后台代码中的事件。

  • 它还消除了“FilterOut”属性,这是一种人为的仅限于GUI的属性,因此真正破坏了MVVM。除非您计划对其进行序列化,否则我不希望在我的ViewModel中看到它,当然也不希望在我的Model中看到它。

  • 在我的示例中,我使用的是“Filter In”,而不是“Filter Out”。对我来说(在大多数情况下),应用的过滤器是我想要看到的东西更合乎逻辑。如果你真的想要过滤掉某些东西,只需否定包含子句(即 item => ! Item.Text.Contains(...))。

  • 您可能有一种更集中的方法在ViewModel中进行设置。重要的是要记住,当您更改FilterText时,还需要通知AllFilteredItems集合。我在这里内联处理了它,但您也可以处理PropertyChanged事件,并在e.PropertyName为FilterText时调用PropertyChanged。

如果您需要任何澄清,请告诉我。


2
我刚刚发现了一个更加优雅的解决方案。不需要像被接受的答案建议的那样在你的ViewModel中创建一个ICollectionView并设置绑定,而是使用以下代码:<CollectionViewSource>
ItemsSource={Binding Path=YourCollectionViewSourceProperty}

更好的方法是在您的ViewModel中创建一个CollectionViewSource属性。然后按照以下方式绑定您的ItemsSource:
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

注意.View的添加。这样ItemsSource绑定仍然会在CollectionViewSource发生更改时得到通知,而且您永远不必手动调用Refresh()ICollectionView
注意:我无法确定为什么会出现这种情况。如果直接绑定到CollectionViewSource属性,则绑定失败。但是,如果在XAML文件的Resources元素中定义CollectionViewSource并直接绑定到资源键,则绑定正常工作。我唯一能猜测的是,当您完全在XAML中完成它时,它知道您真正想要绑定到CollectionViewSource.View值,并相应地在幕后绑定它(多么有帮助! :/)。

2
如果我理解得正确,您要求的是:
在您的FilterText属性的设置部分,只需对您的CollectionView调用Refresh()即可。

3
嗨。那样做是可行的,但在MVVM中不允许这样做 - FilterText属性在ViewModel中,CollectionView在View中,而ViewModel不应该知道View的任何信息。 - Pieter Müller
1
晚回答了。我建议这样做,因为我将我的集合视图也作为属性放在了我的ViewModel中。 - Dummy01

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接