Триггерный фильтр для CollectionViewSource

Я работаю над настольным приложением WPF с использованием шаблона MVVM.

Я пытаюсь отфильтровать некоторые элементы из ListView на основе текста, набранного в TextBox. Я хочу, чтобы ListView элементы фильтровались при изменении текста.

Я хочу знать, как активировать фильтр при изменении текста фильтра.

ListView привязывается к CollectionViewSource, который привязывается к ObservableCollection на моей ViewModel. 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, который выполняет итерацию по ObservableCollection в моей ViewModel и устанавливает свойство boolean FilteredOut для каждого элемента ViewModel.

Я знаю, что свойство FilteredOut обновляется при изменении текста фильтра, но список не обновляется. Событие фильтра CollectionViewSource запускается только тогда, когда я перезагружаю UserControl, переключаясь с него и обратно.

Я попытался позвонить OnPropertyChanged("AllProjects") после обновления информации о фильтре, но это не решило мою проблему. («AllProjects» - это свойство ObservableCollection в моей модели просмотра, к которому привязывается CollectionViewSource.)

Как я могу заставить CollectionViewSource повторно фильтровать себя при изменении значения FilterText TextBox?

Большое спасибо


person Pieter Müller    schedule 24.06.2011    source источник
comment
Кроме того, есть ли способ вызвать метод фильтра (bool Include(object o)) в моей модели ViewModel напрямую, поэтому мне не нужно иметь обработчик событий в фоновом коде?   -  person Pieter Müller    schedule 24.06.2011


Ответы (6)


Не создавайте 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 on кнопки в представлении методу 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>
person Robert Rossney    schedule 24.06.2011
comment
Все ответы, данные до сих пор, были хорошими. Я отмечаю это, так как он решает проблему наиболее MVVMish способом, а также помогает мне с проблемой пользовательской сортировки, с которой я столкнулся. Спасибо. - person Pieter Müller; 26.06.2011
comment
Роберт, успешно ли вы реализовали это ранее? Кажется, я не могу заставить его работать. - person Pieter Müller; 28.06.2011
comment
Все мои реализации встроены в приложения, которые слишком сложны, чтобы просто вытащить и опубликовать, поэтому я создал для вас небольшое демонстрационное приложение. Смотрите мою правку. - person Robert Rossney; 28.06.2011
comment
Огромное спасибо за этот замечательный пример. Я пытался создать экземпляр CollectionView через его конструктор, а не через CollectionViewSource.GetDefaultView(), что немедленно решило мою проблему. Мне кажется, что эти вещи не очень хорошо документированы. Вы мне очень помогли! :-) - person Pieter Müller; 29.06.2011
comment
Отлично! Но есть предупреждение, что обработчик событий CanExecuteChanged никогда не используется. Можем ли мы остановить это предупреждение? - person Andrew Truckle; 28.06.2016
comment
Стоит отметить, что приведенный здесь пример довольно полный, и что мне нужна была только его часть, чтобы привести мой код туда, где он должен быть. Поскольку у меня уже был отсортированный список и он работал, все, что мне было нужно, - это понятие связанного свойства и триггера фильтрации, который назначает Predicate. - person DonBoitnott; 01.12.2017
comment
Отличный ответ, через 7 лет после публикации (и через 2 года после редактирования) он по-прежнему актуален. Спасибо и хорошей работы. - person dtoland; 03.04.2018

В настоящее время вам часто не нужно явно запускать обновления. CollectionViewSource реализует _2 _, который обновляется автоматически, если 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>
person Drew Noakes    schedule 09.06.2015
comment
хотел бы сказать, что это было добавлено в wpf с .net 4.5 - person Ahmed Fwela; 11.08.2015
comment
Это кажется немного недальновидным. Нет пользовательских привязок? Скорее всего, если у вас есть набор элементов в представлении, вы измените какое-то значение в родительской модели представления (например, текст фильтра или логические флаги фильтра). Не только свойство фильтруемых элементов коллекции. - person Trevor Elliott; 15.08.2016
comment
В WPF с .NET 4.6.1 ICollectionViewLiveShaping не реализован. - person 15ee8f99-57ff-4f92-890c-b56153; 29.09.2017
comment
@EdPlunkett, что означает ваш комментарий? Я пытаюсь использовать эту функцию, и ничего не происходит при изменении любого из соответствующих свойств. Должен ли я сейчас реализовывать это вручную? - person themightylc; 21.12.2017
comment
@themightylc У меня нет хорошего ответа на этот вопрос. CollectionViewSource имеет те же свойства и методы, что и ICollectionViewLiveShaping, но не реализует этот интерфейс, и мне неясно, как и можно ли заставить его работать. - person 15ee8f99-57ff-4f92-890c-b56153; 21.12.2017
comment
@EdPlunkett Еще не видел вашего ответа, извините и спасибо. Я не помню, в чем была моя настоящая проблема, но я проверил свою реализацию и использую свой собственный класс, унаследованный от CollectionViewSource с DependencyProperties, который запускает .Refresh () - это работает нормально, но также - очевидно - я не мог заставить LiveShaping работать в 4.6.1. И если это может быть достигнуто так эффективно с такими небольшими усилиями, я не понимаю, зачем мне беспокоиться :) - person themightylc; 21.01.2018
comment
Это сработало как шарм. Мой ItemSource имеет тип ObservableCollection. Свойство имеет тип bool, который при изменении должен обновлять CollectionViewSource. - person Devid; 17.01.2019
comment
ИМХО, это правильный путь. Принятый ответ потребует зависимости от структуры представления в ваших моделях представления, и я стараюсь держать это подальше от моих моделей представления, чтобы мне было легче заменить слой пользовательского интерфейса позже (например, на структуру Uno) . - person James B; 20.02.2020

CollectionViewSource.View.Refresh();

Таким образом переоценивается CollectionViewSource.Filter!

person tuxy42    schedule 10.06.2015

Возможно, вы упростили свой View в своем вопросе, но, как написано, вам действительно не нужен 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», которое является искусственным свойством, предназначенным только для графического интерфейса пользователя, и, таким образом, действительно нарушает работу MVVM. Если вы не планируете сериализовать это, я бы не хотел, чтобы это было в моей ViewModel, и уж тем более в моей модели.

  • В моем примере я использую «Фильтр на входе», а не на «Фильтр на выходе». Мне кажется более логичным (в большинстве случаев), что применяемый мной фильтр - это то, что я действительно хочу видеть. Если вы действительно хотите отфильтровать вещи, просто исключите предложение Contains (например, item =>! Item.Text.Contains (...)).

  • У вас может быть более централизованный способ создания наборов в вашей модели просмотра. Важно помнить, что при изменении FilterText вам также необходимо уведомить свою коллекцию AllFilteredItems. Я сделал это встроенным здесь, но вы также можете обработать событие PropertyChanged и вызвать PropertyChanged, когда e.PropertyName имеет значение FilterText.

Пожалуйста, дайте мне знать, если вам нужны пояснения.

person Wonko the Sane    schedule 24.06.2011

Если я хорошо понял, о чем вы спрашиваете:

В установленной части вашего FilterText свойства просто вызовите Refresh() на свой CollectionView.

person Dummy01    schedule 24.06.2011
comment
Привет. Да, это сработает, но это недопустимо в MVVM - свойство FilterText находится в ViewModel, CollectionView находится в View, а ViewModel не должен иметь никаких сведений о View. - person Pieter Müller; 24.06.2011
comment
Поздний ответ. Я предложил это, потому что я поместил свои представления коллекции также в свою ViewModel как свойства. - person Dummy01; 25.06.2011

Я только что обнаружил гораздо более элегантное решение этой проблемы. Вместо создания ICollectionView в вашей ViewModel (как следует из принятого ответа) и установки вашей привязки на

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

Лучше всего создать свойство CollectionViewSource в вашей модели просмотра. Затем свяжите свой ItemsSource следующим образом

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

Обратите внимание на добавление .View. Таким образом, привязка ItemsSource по-прежнему уведомляется всякий раз, когда есть изменения в CollectionViewSource, и вам никогда не придется вручную вызывать Refresh() на ICollectionView

Примечание: я не могу определить, почему это так. Если вы выполняете привязку напрямую к свойству CollectionViewSource, привязка не выполняется. Однако, если вы определяете CollectionViewSource в элементе Resources файла XAML и выполняете привязку непосредственно к ключу ресурса, привязка работает нормально. Единственное, о чем я могу догадаться, это то, что когда вы делаете это полностью в XAML, он знает, что вы действительно хотите привязаться к значению CollectionViewSource.View, и привязывает его для вас за кулисами (как полезно!: /).

person MoMo    schedule 10.10.2014