Триггерный фильтр на CollectionViewSource - PullRequest
43 голосов
/ 24 июня 2011

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

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

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

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

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

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

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

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

Ответы [ 6 ]

70 голосов
/ 24 июня 2011

Не создавайте CollectionViewSource в вашем представлении. Вместо этого создайте свойство типа ICollectionView в вашей модели представления и привяжите ListView.ItemsSource к нему.

Как только вы это сделаете, вы можете поместить логику в установщик свойства FilterText, который вызывает Refresh() для ICollectionView всякий раз, когда пользователь меняет его.

Вы обнаружите, что это также упрощает проблему сортировки: вы можете встроить логику сортировки в модель представления, а затем представить команды, которые представление может использовать.

EDIT

Вот довольно простая демонстрация динамической сортировки и фильтрации представления коллекции с использованием 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>
22 голосов
/ 09 июня 2015

В настоящее время вам часто не нужно явно запускать обновления.CollectionViewSource реализует ICollectionViewLiveShaping, который автоматически обновляется, если IsLiveFilteringRequested имеет значение true, на основе полей в его коллекции 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>
5 голосов
/ 24 июня 2011

Возможно, вы упростили 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 и, конечно, не в моей модели.

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

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

Пожалуйста, дайте мне знать, если вам нужны какие-либо разъяснения.

4 голосов
/ 10 июня 2015
CollectionViewSource.View.Refresh();

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

1 голос
/ 10 октября 2014

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty}

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

ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    

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

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

1 голос
/ 24 июня 2011

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

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

...