wpf 在 CollectionViewSource 上触发过滤器

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/6466917/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 22:56:02  来源:igfitidea点击:

Trigger Filter on CollectionViewSource

wpfxamlmvvmfiltercollectionviewsource

提问by Pieter Müller

I am working on a WPF desktop application using the MVVM pattern.

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

I am trying to filter some items out of a ListViewbased on the text typed in a TextBox. I want the ListViewitems to be filtered as I change the text.

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

I want to know how to trigger the filter when the filter text changes.

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

The ListViewbinds to a CollectionViewSource, which binds to the ObservableCollectionon my ViewModel. The TextBoxfor the filter text binds to a string on the ViewModel, with UpdateSourceTrigger=PropertyChanged, as it should be.

ListView绑定到CollectionViewSource,它绑定到ObservableCollection我的视图模型。该TextBox用于过滤的文本绑定到一个字符串的视图模型,用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}" />

The Filter="CollectionViewSource_Filter"links to an event handler in the code behind, which simply calls a filter method on the ViewModel.

Filter="CollectionViewSource_Filter"后面代码中的事件处理程序的链接,它只是调用 ViewModel 上的过滤器方法。

Filtering is done when the value of FilterText changes - the setter for the FilterText property calls a FilterList method that iterates over the ObservableCollectionin my ViewModel and sets a booleanFilteredOut property on each item ViewModel.

当 FilterText 的值发生变化时,过滤就完成了——FilterText 属性的设置器调用一个 FilterList 方法,该方法ObservableCollection在我的 ViewModel中迭代并boolean在每个项目 ViewModel 上设置一个FilteredOut 属性。

I know the FilteredOut property is updated when the filter text changes, but the List does not refresh. The CollectionViewSourcefilter event is only fired when I reload the UserControl by switching away from it and back again.

我知道 FilteredOut 属性会在过滤器文本更改时更新,但列表不会刷新。该CollectionViewSource过滤器的事件是,当我切换远离它,并再次重新加载用户控件只解雇。

I've tried calling OnPropertyChanged("AllProjects")after updating the filter info, but it did not solve my problem. ("AllProjects" is the ObservableCollectionproperty on my ViewModel to which the CollectionViewSourcebinds.)

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

How can I get the CollectionViewSourceto refilter itself when the value of the FilterText TextBoxchanges?

我怎样才能获得CollectionViewSource到重新过滤本身当FilterText的价值TextBox变化?

Many thanks

非常感谢

回答by Robert Rossney

Don't create a CollectionViewSourcein your view. Instead, create a property of type ICollectionViewin your view model and bind ListView.ItemsSourceto it.

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

Once you've done this, you can put logic in the FilterTextproperty's setter that calls Refresh()on the ICollectionViewwhenever the user changes it.

一旦你做到了这一点,你可以把逻辑在FilterText属性的setter方法调用Refresh()ICollectionView,每当用户改变它。

You'll find that this also simplifies the problem of sorting: you can build the sorting logic into the view model and then expose commands that the view can use.

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

EDIT

编辑

Here's a pretty straightforward demo of dynamic sorting and filtering of a collection view using MVVM. This demo doesn't implement FilterText, but once you understand how it all works, you shouldn't have any difficulty implementing a FilterTextproperty and a predicate that uses that property instead of the hard-coded filter that it's using now.

这是使用 MVVM 对集合视图进行动态排序和过滤的一个非常简单的演示。这个演示没有实现FilterText,但是一旦你理解了它是如何工作的,你应该不会有任何困难实现一个FilterText属性和一个使用该属性而不是它现在使用的硬编码过滤器的谓词。

(Note also that the view model classes here don't implement property-change notification. That's just to keep the code simple: as nothing in this demo actually changes property values, it doesn't need property-change notification.)

(还要注意,这里的视图模型类没有实现属性更改通知。这只是为了保持代码简单:因为这个演示中没有实际更改属性值,它不需要属性更改通知。)

First a class for your items:

首先是您的物品的类:

public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Now, a view model for the application. There are three things going on here: first, it creates and populates its own ICollectionView; second, it exposes an ApplicationCommand(see below) that the view will use to execute sorting and filtering commands, and finally, it implements an Executemethod that sorts or filters the view:

现在,应用程序的视图模型。这里发生了三件事:首先,它创建并填充自己的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;
        }
    }
}

Sorting kind of sucks; you need to implement an IComparer:

排序很糟糕;你需要实现一个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);
        }
    }
}

To trigger the Executemethod in the view model, this uses an ApplicationCommandclass, which is a simple implementation of ICommandthat routes the CommandParameteron buttons in the view to the view model's Executemethod. I implemented it this way because I didn't want to create a bunch of RelayCommandproperties in the application view model, and I wanted to keep all the sorting/filtering in one method so that it was easy to see how it's done.

为了触发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;
}

Finally, here's the MainWindowfor the application:

最后,这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>

回答by Drew Noakes

Nowadays, you often don't need to explicitly trigger refreshes. CollectionViewSourceimplements ICollectionViewLiveShapingwhich updates automatically if IsLiveFilteringRequestedis true, based upon the fields in its LiveFilteringPropertiescollection.

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

An example in XAML:

XAML 中的一个示例:

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

回答by Wonko the Sane

Perhaps you've simplified your View in your question, but as written, you don't really need a CollectionViewSource - you can bind to a filtered list directly in your ViewModel (mItemsToFilter is the collection that is being filtered, probably "AllProjects" in your example):

也许你已经在你的问题中简化了你的视图,但正如所写的那样,你真的不需要 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"));
        }
    }
}

Your View would then simply be:

您的视图将只是:

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

Some quick notes:

一些快速说明:

  • This eliminates the event in the code behind

  • It also eliminates the "FilterOut" property, which is an artificial, GUI-only property and thus really breaks MVVM. Unless you plan to serialize this, I wouldn't want it in my ViewModel, and certainly not in my Model.

  • In my example, I use a "Filter In" rather than a "Filter Out". It seems more logical to me (in most cases) that the filter I am applying are things I dowant to see. If you really want to filter things out, just negate the Contains clause (i.e. item => ! Item.Text.Contains(...)).

  • You may have a more centralized way of doing your Sets in your ViewModel. The important thing to remember is that when you change the FilterText, you also need to notify your AllFilteredItems collection. I did it inline here, but you could also handle the PropertyChanged event and call PropertyChanged when the e.PropertyName is FilterText.

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

  • 它还消除了“FilterOut”属性,这是一个人工的、仅限 GUI 的属性,因此真正破坏了 MVVM。除非你打算序列化它,否则我不希望它出现在我的 ViewModel 中,当然也不会出现在我的模型中。

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

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

Please let me know if you need any clarifications.

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

回答by tuxy42

CollectionViewSource.View.Refresh();

CollectionViewSource.Filter is reevaluated in this way!

CollectionViewSource.Filter 就是这样重新评估的!

回答by Dummy01

If I understood well what you are asking:

如果我理解你在问什么:

In the set part of your FilterTextproperty just call Refresh()to your CollectionView.

在您的FilterText财产的 set 部分中,只需调用Refresh()您的CollectionView.

回答by MoMo

I just discovered a much more elegant solution to this issue. Insteadof creating a ICollectionViewin your ViewModel (as the accepted answer suggests) and setting your binding to

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

The better way is to create a CollectionViewSourceproperty in your ViewModel. Then bind your ItemsSourceas follows

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

Notice the addition of .ViewThis way the ItemsSourcebinding is still notified whenever there is a change to the CollectionViewSourceand you never have to manually call Refresh()on the ICollectionView

注意添加.View这样的ItemsSource结合仍然通知每当有变化的CollectionViewSource你永远不必手动调用Refresh()ICollectionView

Note: I can't determine why this is the case. If you bind directly to a CollectionViewSourceproperty the binding fails. However, if you define a CollectionViewSourcein your Resourceselement of a XAML file and you bind directly to the resource key, the binding works fine. The only thing I can guess is that when you do it completely in XAML it knows you really want to bind to the CollectionViewSource.View value and binds it for you acourdingly behind the scenes (how helpful! :/) .

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