Trigger Filter on CollectionViewSource

前端 未结 6 998
庸人自扰
庸人自扰 2020-12-04 08:27

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

I am trying to filter some items out of a ListView based on the text typed in a

相关标签:
6条回答
  • 2020-12-04 09:01

    Don't create a CollectionViewSource in your view. Instead, create a property of type ICollectionView in your view model and bind ListView.ItemsSource to it.

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

    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 FilterText property and a predicate that uses that property instead of the hard-coded filter that it's using now.

    (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 Execute method that sorts or filters the view:

    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:

    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 Execute method in the view model, this uses an ApplicationCommand class, which is a simple implementation of ICommand that routes the CommandParameter on buttons in the view to the view model's Execute method. I implemented it this way because I didn't want to create a bunch of RelayCommand properties 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.

    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 MainWindow for the application:

    <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>
    
    0 讨论(0)
  • 2020-12-04 09:09

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

    ItemsSource={Binding Path=YourCollectionViewSourceProperty}
    

    The better way is to create a CollectionViewSource property in your ViewModel. Then bind your ItemsSource as follows

    ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    
    

    Notice the addition of .View This way the ItemsSource binding is still notified whenever there is a change to the CollectionViewSource and you never have to manually call Refresh() on the ICollectionView

    Note: I can't determine why this is the case. If you bind directly to a CollectionViewSource property the binding fails. However, if you define a CollectionViewSource in your Resources element 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! :/) .

    0 讨论(0)
  • 2020-12-04 09:10

    If I understood well what you are asking:

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

    0 讨论(0)
  • 2020-12-04 09:18
    CollectionViewSource.View.Refresh();
    

    CollectionViewSource.Filter is reevaluated in this way!

    0 讨论(0)
  • 2020-12-04 09:20

    Nowadays, you often don't need to explicitly trigger refreshes. CollectionViewSource implements ICollectionViewLiveShaping which updates automatically if IsLiveFilteringRequested is true, based upon the fields in its LiveFilteringProperties collection.

    An example in XAML:

      <CollectionViewSource
             Source="{Binding Items}"
             Filter="FilterPredicateFunction"
             IsLiveFilteringRequested="True">
        <CollectionViewSource.LiveFilteringProperties>
          <system:String>FilteredProperty1</system:String>
          <system:String>FilteredProperty2</system:String>
        </CollectionViewSource.LiveFilteringProperties>
      </CollectionViewSource>
    
    0 讨论(0)
  • 2020-12-04 09:25

    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):

    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 do want 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.

    Please let me know if you need any clarifications.

    0 讨论(0)
提交回复
热议问题