在 CollectionViewSource 上触发过滤器

Posted

技术标签:

【中文标题】在 CollectionViewSource 上触发过滤器【英文标题】:Trigger Filter on CollectionViewSource 【发布时间】:2011-09-21 22:54:45 【问题描述】:

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

我正在尝试根据TextBox 中键入的文本从ListView 中过滤出一些项目。我希望在更改文本时过滤 ListView 项目。

我想知道当过滤器文本发生变化时如何触发过滤器。

ListView 绑定到 CollectionViewSource,后者绑定到我的 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 属性的设置器调用一个 FilterList 方法,该方法在我的 ViewModel 中迭代 ObservableCollection 并在每个项目 ViewModel 上设置一个 boolean FilteredOut 属性。

我知道 FilteredOut 属性会在过滤器文本更改时更新,但 List 不会刷新。 CollectionViewSource 过滤器事件只有在我重新加载 UserControl 时才会触发,方法是从它切换回来再切换回来。

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

当 FilterText TextBox 的值发生变化时,如何让 CollectionViewSource 重新过滤自身?

非常感谢

【问题讨论】:

另外,有没有办法直接在我的 ViewModel 上调用过滤器方法(bool Include(object o)),所以我不需要在代码隐藏中使用事件处理程序? 【参考方案1】:

不要在您的视图中创建CollectionViewSource。相反,在您的视图模型中创建ICollectionView 类型的属性并将ListView.ItemsSource 绑定到它。

完成此操作后,您可以将逻辑放入FilterText 属性的设置器中,该设置器在用户更改时调用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>

【讨论】:

到目前为止给出的所有答案都很好。我正在标记这个,因为它以最 MVVMish 的方式解决了问题,并且还帮助我解决了我一直遇到的自定义排序问题。谢谢。 罗伯特,你之前有成功实现过这个吗?我似乎无法让它工作。 我的实现都嵌入在应用程序中,这些应用程序过于复杂而无法直接拉出并发布,因此我为您准备了一个小演示应用程序。查看我的编辑。 非常非常感谢这个很好的例子。我试图通过其构造函数来实例化 CollectionView,而不是通过 CollectionViewSource.GetDefaultView(),这立即解决了我的问题。在我看来,这些事情没有很好的记录。你帮了我大忙! :-) 这太棒了!但是有一个警告,事件处理程序CanExecuteChanged 从未使用过。我们可以停止这个警告吗?【参考方案2】:

如今,您通常不需要显式触发刷新。 CollectionViewSource 实现 ICollectionViewLiveShaping,如果 IsLiveFilteringRequested 为真,则根据其 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>

【讨论】:

想说这是在 .net 4.5 的 wpf 中添加的 这似乎有点短视。没有自定义绑定?如果您在视图中有一组项目,则可能会更改父视图模型上的某些值(例如过滤器文本或布尔过滤器标志)。不仅仅是被过滤的集合中项目的属性。 在带有 .NET 4.6.1 的 WPF 中,ICollectionViewLiveShaping 未实现。 @EdPlunkett 你的评论意味着什么?我正在尝试使用此功能,但当任何相关属性发生更改时,什么都没有发生。我现在必须手动实现吗? 恕我直言,这是要走的路。接受的答案将需要依赖于您的视图模型中的表示框架,并且我试图将其远离我的视图模型,以便我可以在以后更轻松地换出 UI 层(例如,到 Uno 框架) .【参考方案3】:
CollectionViewSource.View.Refresh();

CollectionViewSource.Filter 以这种方式重新评估!

【讨论】:

【参考方案4】:

也许你在你的问题中简化了你的视图,但正如写的那样,你真的不需要 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 中,当然也不会出现在我的模型中。

在我的示例中,我使用“过滤器输入”而不是“过滤器输出”。在我看来(在大多数情况下)我正在应用的过滤器是我确实想看到的东西,这似乎更合乎逻辑。如果您真的想过滤掉内容,只需否定 Contains 子句(即 item => !Item.Text.Contains(...))。

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

如果您需要任何说明,请告诉我。

【讨论】:

【参考方案5】:

如果我明白你在问什么:

在您的FilterText 属性的设置部分中,只需将Refresh() 调用到您的CollectionView

【讨论】:

嗨。可以,是的,但是在 MVVM 中是不允许的 - FilterText 属性在 ViewModel 中,CollectionView 在 View 中,并且 ViewModel 不应该对 View 有任何了解。 迟到的答案。我建议这样做,因为我也将我的集合视图作为属性放在我的 ViewModel 中。【参考方案6】:

我刚刚发现了一个更优雅的解决方案来解决这个问题。 而不是在您的 ViewModel 中创建 ICollectionView(正如公认的答案所建议的那样)并将您的绑定设置为

ItemsSource=Binding Path=YourCollectionViewSourceProperty

更好的方法是在您的 ViewModel 中创建一个 CollectionViewSource 属性。然后按如下方式绑定你的ItemsSource

ItemsSource=Binding Path=YourCollectionViewSourceProperty.View    

注意添加了 .View 这样ItemsSource 绑定仍然会在CollectionViewSource 发生更改时收到通知,并且您无需手动调用Refresh()ICollectionView

注意:我无法确定为什么会这样。如果您直接绑定到 CollectionViewSource 属性,则绑定失败。但是,如果您在 XAML 文件的 Resources 元素中定义 CollectionViewSource 并直接绑定到资源键,则绑定工作正常。我唯一能猜到的是,当你完全在 XAML 中完成它时,它知道你真的想要绑定到 CollectionViewSource.View 值并在幕后为你绑定它(多么有帮助!:/)。

【讨论】:

以上是关于在 CollectionViewSource 上触发过滤器的主要内容,如果未能解决你的问题,请参考以下文章

[Wpf]在C#中添加 collectionViewSource

与 CollectionViewSource 绑定时,DesignTime 数据未显示在 Blend 中

CollectionViewSource+PropertyGroupDescription - 组中的项目数

在datagrid celltemplate中绑定collectionviewsource

在 ViewModel 中使用 CollectionViewSource 的正确方法

当ObservableCollection发生变化时更新CollectionViewSource WP7