首页 > 解决方案 > 通过 MVVM 动态过滤绑定到 SelectedItem 的 CollectionViewSource

问题描述

我一直在深入研究 WPF 中的一些项目,遇到了一个我无法找到直接相关解决方案的障碍。

本质上,我想动态过滤 SelectedItem 的子属性(通过在过滤器框中输入的文本,类似于 的内容.Contains(filter))。UI 在示例项目中正确显示,但是在尝试从 SO 或其他可能的每个命中实施解决方案之后,我出现了空白,或者对 MVVM 模式做出了严重的妥协。

父项:

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
    }

视图模型:

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但无法使它们动态化。似乎大多数示例/教程仅直接处理父项或静态过滤器。

在非 MVVM 思维方式中,我正在考虑让交互触发器将所选项目驱动回 ViewModel,然后创建一个过滤后的 ICollectionView,ChildItems ListView 将绑定到该 ICollectionView,但似乎我不能成为唯一的人尝试这个,并且必须有一种更简单的 MVVM 绑定友好方式。

标签: c#wpfmvvm

解决方案


以下示例显示了在集合上实现实时过滤的简单解决方案:

个人.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; }
}

视图模型.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; }
}

主窗口.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>

更新

您似乎在过滤子项时遇到问题。以下示例更适合您的场景:

数据项.cs

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

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

视图模型.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; }
}

主窗口.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>

推荐阅读