Подтвердить что ты не робот

Фильтр триггеров на 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 обновляется при изменении текста фильтра, но List не обновляется. Событие фильтра CollectionViewSource запускается только при перезагрузке UserControl путем перехода от него и обратно.

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

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

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

4b9b3361

Ответ 1

Не создавайте 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>

Ответ 2

В настоящее время вам часто не нужно явно запускать обновления. 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>

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

Я только что обнаружил гораздо более элегантное решение этой проблемы. Вместо создания 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 и привязывает его для вас за кулисами (насколько полезно!).

Ответ 6

CollectionViewSource.View.Refresh();

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