Динамическая фильтрация CollectionViewSource, привязанного к SelectedItem, через MVVM - PullRequest
0 голосов
/ 29 февраля 2020

Я копался в некоторых проектах в WPF и столкнулся с препятствием, для которого я не смог найти напрямую связанного решения.

По сути, я хочу отфильтровать дочернее свойство SelectedItem динамически (через текст, введенный в поле фильтра, что-то вроде .Contains(filter)). Пользовательский интерфейс отображается правильно в примере проекта, но после попытки реализовать решения от каждого возможного попадания на SO или иным образом я потерпел неудачу или сделал серьезные компромиссы с шаблоном MVVM.

ParentItem :

public class ParentItem
    {
        public string Name { get; set; }
        public List<string> ChildItems { get; set; }
        public DateTime CreatedOn { get; set; }
        public bool IsActive { get; set; }
        public ParentItemStatus Status { get; set; }
    }

    public enum ParentItemStatus
    {
        Status_One,
        Status_Two
    }

ViewModel:

public class MainWindowViewModel : ViewModelBase
    {
        public ObservableCollection<ParentItem> ParentItems { get; set; }

        public MainWindowViewModel()
        {
            ParentItems = new ObservableCollection<ParentItem>();

            LoadDummyParentItems();
        }

        private ICommand _filterChildrenCommand;
        public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));

        private bool CanFilterChildren(string filter)
        {
            //TODO: Check for selected item in real life.
            return filter.Length > 0;
        }

        private void FilterChildren(string filter)
        {
            //TODO: Filter?
        }

        private void LoadDummyParentItems()
        {
            for (var i = 0; i < 20; i++)
            {
                ParentItems.Add(new ParentItem()
                {
                    Name = $"Parent Item {i}",
                    CreatedOn = DateTime.Now.AddHours(i),
                    IsActive = i % 2 == 0 ? true : false,
                    Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
                    ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
                });
            }
        }
    }

Главное окно:

<Window x:Class="FilteringDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FilteringDemo.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <CollectionViewSource x:Key="ChildItemsViewSource" Source="{Binding ElementName=ItemList, Path=SelectedItem.ChildItems}" />
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width=".25*"/>
            <ColumnDefinition Width=".75*"/>
        </Grid.ColumnDefinitions>

        <ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectionMode="Single">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
                <TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
                <TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
                <TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
            </Grid>

            <ListView Grid.Row="1" Margin="2" ItemsSource="{Binding Source={StaticResource ChildItemsViewSource}}" />

            <Grid Grid.Row="2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="Contains:" Margin="2" VerticalAlignment="Center"/>
                <TextBox x:Name="ChildFilterInput" Grid.Column="1" Margin="2" />
                <Button Grid.Column="2" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
            </Grid>

        </Grid>
    </Grid>
</Window>

Я пробовал различные реализации добавления обработчика событий Filter на CollectionViewSource, но не смог сделать их динамическими c. Похоже, что большинство примеров / учебных пособий имеют дело только с родительским элементом или фильтрами stati c.

В мышлении, отличном от MVVM, я думал о том, чтобы триггер взаимодействия приводил выбранный элемент обратно в ViewModel, а затем создайте отфильтрованный ICollectionView, с которым связывается ChildItems ListView, но кажется, что я не могу быть единственным человеком, пытающимся это сделать, и что должен быть более простой способ дружественного связывания MVVM.

Ответы [ 2 ]

1 голос
/ 29 февраля 2020

В следующем примере показано простое решение для реализации фильтрации в реальном времени для коллекции:

Person.cs

class Person
{
  public Person(string firstName, string lastName)
  {
    this.FirstName = firstName;
    this.LastName = lastName;
  }

  public string FirstName { get; set; }
  public string LastName { get; set; }
}

ViewModel.cs

class ViewModel
{
  public ViewModel()
  {
    this.Persons = new ObservableCollection<Person>()
    {
      new Person("Derek", "Zoolander"),
      new Person("Tony", "Montana"),
      new Person("John", "Wick"),
      new Person("The", "Dude"),
      new Person("James", "Bond"),
      new Person("Walter", "White")
    };
  }

  private void FilterData(string filterPredicate)
  {
    // Execute live filter
    CollectionViewSource.GetDefaultView(this.Persons).Filter =
        item => (item as Person).FirstName.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
  }

  private string searchPredicate;   
  public string SearchPredicate
  {
    get => this.searchFilter;
    set 
    { 
      this.searchPredicate = value;
      FilterData(value);
    }
  }

  public ObservableCollection<Person> Persons { get; set; }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContext>

  <StackPanel>
    <TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged"} />
    <ListView ItemsSource="{Binding Persons}">
      <ListView.View>
        <GridView>
          <GridView.Columns>
            <GridViewColumn Header="Firstname" DisplayMemberBinding="{Binding FirstName}" />
            <GridViewColumn Header="Lastname" DisplayMemberBinding="{Binding LastName}" />
          </GridView.Columns>
        </GridView>
      </ListView.View>
    </ListView>
  </StackPanel>
</Window>

Обновление

Похоже, у вас проблемы с фильтрацией дочерних элементов , Следующий пример более конкретен c для вашего сценария:

DataItem.cs

class DataItem
{
  public DataItem(string Name)
  {
    this.Name = name;
  }

  public string Name { get; set; }
  public ObservableCollection<DataItem> ChildItems { get; set; }
}

ViewModel.cs

class ViewModel
{
  public ViewModel()
  {
    this.ParentItems = new ObservableCollection<DataItem>()
    {
      new DataItem("Ben Stiller") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Zoolander"), new DataItem("Tropical Thunder") }},
      new DataItem("Al Pacino") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Scarface"), new DataItem("The Irishman") }},
      new DataItem("Keanu Reeves") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("John Wick"), new DataItem("Matrix") }},
      new DataItem("Bryan Cranston") { ChildItems = new ObservableCollection<DataItem>() { new DataItem("Breaking Bad"), new DataItem("Malcolm in the Middle") }}
    };
  }

  private void FilterData(string filterPredicate)
  {
    // Execute live filter
    CollectionViewSource.GetDefaultView(this.SelectedParentItem.ChildItems).Filter =
        item => (item as DataItem).Name.StartsWith(filterPredicate, StringComparison.OrdinalIgnoreCase);
  }

  private string searchPredicate;   
  public string SearchPredicate
  {
    get => this.searchFilter;
    set 
    { 
      this.searchPredicate = value;
      FilterData(value);
    }
  }

  public ObservableCollection<DataItem> ParentItems { get; set; }
  public DataItem SelectedParentItem { get; set; }
}

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContext>

  <StackPanel>
    <ListView ItemsSource="{Binding ParentItems}" 
              SelectedItem="{Binding SelectedParentItem}">
      <ListView.ItemTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Name}"/>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>


    <TextBox Text="{Binding SearchPredicate, UpdateSourceTrigger=PropertyChanged}" />
    <ListView ItemsSource="{Binding SelectedParentItem.ChildItems}" />
  </StackPanel>
</Window>
0 голосов
/ 29 февраля 2020

Используя примеры из @ BionicCode, я добавил свойство SelectedParentItem INP C в ViewModel, выполнил фильтрацию по нему через CollectionViewSource.Filter и привязал ChildItems ListView к SelectedParentItem.ChildItems.

* 1005. * Я не привязывал свойство текстового поля, измененное к вспомогательному полю в ВМ, как в примере с @ BionicCode, так как «настоящие» ChildItems могли быть в середине 10000-х годов, и я не хотел, чтобы оно фильтровалось при каждом нажатии клавиши. Таким образом, этот ответ реализует кнопку фильтра и текстовое поле, а CanFilterChildren выполняет правильную проверку на ноль.

MainWindowViewModel.cs :

public class MainWindowViewModel : ViewModelBase
    {
        public ObservableCollection<ParentItem> ParentItems { get; set; }

        private ParentItem _selectedParentItem;
        public ParentItem SelectedParentItem
        {
            get { return _selectedParentItem; }
            set { SetProperty(ref _selectedParentItem, value); }
        }

        public MainWindowViewModel()
        {
            ParentItems = new ObservableCollection<ParentItem>();

            LoadDummyParentItems();
        }

        private ICommand _filterChildrenCommand;
        public ICommand FilterChildrenCommand => _filterChildrenCommand ?? (_filterChildrenCommand = new RelayCommand(param => FilterChildren((string)param), param => CanFilterChildren((string)param)));

        private bool CanFilterChildren(string filter) => SelectedParentItem != null && filter.Length > 0;

        private void FilterChildren(string filter)
        {
            CollectionViewSource.GetDefaultView(SelectedParentItem.ChildItems).Filter = item => (item as string).Contains(filter);
        }

        private void LoadDummyParentItems()
        {
            for (var i = 0; i < 20; i++)
            {
                ParentItems.Add(new ParentItem()
                {
                    Name = $"Parent Item {i}",
                    CreatedOn = DateTime.Now.AddHours(i),
                    IsActive = i % 2 == 0 ? true : false,
                    Status = i % 2 == 0 ? ParentItemStatus.Status_Two : ParentItemStatus.Status_One,
                    ChildItems = new List<string>() { $"Child one_{i}", $"Child two_{i}", $"Child three_{i}", $"Child four_{i}" }
                });
            }
        }
    }

MainWindow.xaml.cs :

public partial class MainWindow : Window
    {
        private readonly MainWindowViewModel _viewModel;

        public MainWindow()
        {
            InitializeComponent();
            _viewModel = new MainWindowViewModel();
            this.DataContext = _viewModel;
        }
    }

MainWindow.xaml :

<Window x:Class="FilteringDemo.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FilteringDemo.Views"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width=".25*"/>
            <ColumnDefinition Width=".75*"/>
        </Grid.ColumnDefinitions>

        <ListView x:Name="ItemList" Grid.Column="0" Margin="2" ItemsSource="{Binding ParentItems}" SelectedItem="{Binding SelectedParentItem, Mode=OneWayToSource}" SelectionMode="Single">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <Grid Grid.Column="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="1*"/>
                </Grid.ColumnDefinitions>

                <TextBlock Grid.Column="0" Text="{Binding ElementName=ItemList, Path=SelectedItem.Name}" Margin="2"/>
                <TextBlock Grid.Column="1" Text="{Binding ElementName=ItemList, Path=SelectedItem.CreatedOn}" Margin="2"/>
                <TextBlock Grid.Column="2" Text="{Binding ElementName=ItemList, Path=SelectedItem.IsActive}" Margin="2"/>
                <TextBlock Grid.Column="3" Text="{Binding ElementName=ItemList, Path=SelectedItem.Status}" Margin="2"/>
            </Grid>

            <ListView Grid.Row="1" Margin="2" ItemsSource="{Binding SelectedParentItem.ChildItems}" />

            <Grid Grid.Row="2">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="1*"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>

                <TextBox x:Name="ChildFilterInput" Grid.Column="0" Margin="2">
                    <TextBox.InputBindings>
                        <KeyBinding Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}" Key="Return" />
                    </TextBox.InputBindings>
                </TextBox>
                <Button Grid.Column="1" Content="Filter" Width="100" Margin="2" Command="{Binding FilterChildrenCommand}" CommandParameter="{Binding ElementName=ChildFilterInput, Path=Text}"/>
            </Grid>

        </Grid>
    </Grid>
</Window>

ViewModelBase.cs :

public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
        {
            if (!EqualityComparer<T>.Default.Equals(field, newValue))
            {
                field = newValue;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
                return true;
            }
            return false;
        }
    }

RelayCommand.cs :

public class RelayCommand : ICommand
    {
        private Predicate<object> _canExecuteMethod;
        private Action<object> _executeMethod;

        public RelayCommand(Action<object> executeMethod, Predicate<object> canExecuteMethod = null)
        {
            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecuteMethod == null ? true : _canExecuteMethod(parameter);
        }

        public void Execute(object parameter)
        {
            _executeMethod(parameter);
        }
    }

ParentItem.cs :

public class ParentItem
    {
        public string Name { get; set; }
        public List<string> ChildItems { get; set; }
        public DateTime CreatedOn { get; set; }
        public bool IsActive { get; set; }
        public ParentItemStatus Status { get; set; }
    }

    public enum ParentItemStatus
    {
        Status_One,
        Status_Two
    }

App.xaml :

<Application x:Class="FilteringDemo.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:FilteringDemo"
             StartupUri="Views/MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

Примечание: Файл MainWindow.xaml был перемещен в папку «Views», поэтому я включаю App.xaml с обновленным StartupUri на случай, если кто-нибудь пытается скопировать и вставить.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...