关于wpf:CollectionViewSource上的触发筛选器

Trigger Filter on CollectionViewSource

我正在使用MVVM模式在WPF桌面应用程序上工作。

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

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

ListView绑定到CollectionViewSource,而CollectionViewSource绑定到我的ViewModel上的ObservableCollection。过滤器文本的TextBox应当与UpdateSourceTrigger=PropertyChanged绑定到ViewModel上的字符串。

1
2
3
4
5
6
7
8
<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上调用filter方法。

当FilterText的值更改时完成过滤-FilterText属性的设置器调用FilterList方法,该方法遍历ViewModel中的ObservableCollection并在每个ViewModel项上设置boolean FilteredOut属性。

我知道当过滤器文本更改时,FilteredOut属性会更新,但列表不会刷新。 CollectionViewSource过滤器事件仅在我通过切换离开并再次返回来重新加载UserControl时才触发。

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

当FilterText TextBox的值更改时,如何获取CollectionViewSource进行自身重新过滤?

非常感谢


不要在视图中创建CollectionViewSource。而是在视图模型中创建类型为ICollectionView的属性,然后将ListView.ItemsSource绑定到该属性。

完成此操作后,可以将逻辑放入FilterText属性的设置器中,该设置器在用户更改ICollectionView时调用ICollectionView上的Refresh()

您会发现这也简化了排序问题:您可以将排序逻辑构建到视图模型中,然后公开视图可以使用的命令。

编辑

这是一个使用MVVM对集合视图进行动态排序和过滤的非常简单的演示。该演示没有实现FilterText,但是一旦您了解了它的工作原理,就可以轻松实现FilterText属性和使用该属性的谓词而不是现在使用的硬编码过滤器。 。

(还要注意,这里的视图模型类没有实现属性更改通知。这只是为了保持代码简单:由于此演示中的任何内容实际上都不会更改属性值,因此不需要属性更改通知。)

首先是您的物品的课程:

1
2
3
4
5
public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

现在,该应用程序的视图模型。这里发生三件事:首先,它创建并填充自己的ICollectionView;其次,它公开了一个ApplicationCommand(请参见下文),该视图将用于执行排序和过滤命令,最后,它实现了一个Execute方法来对视图进行排序或过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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属性,并且希望将所有排序/过滤保持在一个方法中,这样很容易看到它是如何完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<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>


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

XAML中的一个示例:

1
2
3
4
5
6
7
8
9
  <CollectionViewSource
         Source="{Binding Items}"
         Filter="FilterPredicateFunction"
         IsLiveFilteringRequested="True">
    <CollectionViewSource.LiveFilteringProperties>
      <system:String>FilteredProperty1</system:String>
      <system:String>FilteredProperty2</system:String>
    </CollectionViewSource.LiveFilteringProperties>
  </CollectionViewSource>


也许您已经在问题中简化了View,但是按照书面要求,您实际上并不需要CollectionViewSource-您可以直接在ViewModel中绑定到过滤列表(mItemsToFilter是要过滤的集合,可能是" AllProjects"您的示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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"));
        }
    }
}

您的视图将仅仅是:

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

快速注意事项:

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

  • 它还消除了" FilterOut"属性,该属性是人为的,仅用于GUI的属性,因此实际上破坏了MVVM。除非您计划序列化它,否则我不会在ViewModel中使用它,当然也不会在我的Model中使用它。

  • 在我的示例中,我使用"过滤器输入"而不是"过滤器输出"。在大多数情况下,对我来说似乎更合逻辑的是,我正在应用的过滤器是我确实希望看到的东西。如果您确实想过滤掉所有内容,则只需取消Contains子句即可(即item =>!Item.Text.Contains(...))。

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

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


1
CollectionViewSource.View.Refresh();

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


我刚刚发现了一个更优雅的解决方案。而不是在ViewModel中创建ICollectionView(如接受的答案所示)并将绑定设置为

1
ItemsSource={Binding Path=YourCollectionViewSourceProperty}

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

1
ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}

请注意,添加了.View这样,只要CollectionViewSource发生更改,仍会通知ItemsSource绑定,而您不必手动在ICollectionView上调用Refresh()

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


如果我很了解您的要求:

FilterText属性的set部分中,只需将Refresh()调用到CollectionView