CollectionViewSource上的触发器过滤器

时间:2011-06-24 11:11:09

标签: wpf xaml mvvm filter collectionviewsource

我正在使用MVVM模式处理WPF桌面应用程序。

我正在尝试根据ListView中输入的文字过滤TextBox中的某些项目。我希望在更改文本时过滤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属性的setter调用FilterList方法,该方法迭代ViewModel中的ObservableCollection并在每个项目ViewModel上设置boolean FilteredOut属性。

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

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

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

非常感谢

6 个答案:

答案 0 :(得分:69)

请勿在视图中创建CollectionViewSource。而是在视图模型中创建类型为ICollectionView的属性,并将ListView.ItemsSource绑定到该属性。

完成此操作后,您可以将逻辑放在FilterText属性的设置器中,只要用户更改它,就会在Refresh()上调用ICollectionView

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

修改

这是一个非常简单的使用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}}

答案 1 :(得分:22)

如今,您通常不需要明确触发刷新。 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>

答案 2 :(得分:5)

也许你已经在你的问题中简化了你的View,但是如你所写的,你并不真正需要一个CollectionViewSource - 你可以直接在ViewModel中绑定到一个过滤的列表(mItemsToFilter是被过滤的集合,可能是“所有项目“在你的例子中”:

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中,当然也不在我的模型中。

  • 在我的示例中,我使用“Filter In”而不是“Filter Out”。我(在大多数情况下)我正在应用的过滤器是我希望看到的东西似乎更合乎逻辑。如果你真的想要过滤掉东西,只需取消Contains子句(即item =&gt;!Item.Text.Contains(...))。

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

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

答案 3 :(得分:4)

CollectionViewSource.View.Refresh();

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

答案 4 :(得分:1)

如果我很清楚你在问什么:

FilterText媒体资源的设置部分,只需致电Refresh()即可CollectionView

答案 5 :(得分:1)

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

请注意添加 .View 这样,只要ItemsSource发生更改,您仍然会通知CollectionViewSource绑定,您无需手动调用Refresh()

上的ICollectionView

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