【问题标题】:WPF filter combobox items based on ListView itemsWPF 根据 ListView 项过滤组合框项
【发布时间】:2010-12-29 04:54:03
【问题描述】:

我正在使用由 ListView 和一些 ComboBoxes 组成的 MVVM 设计模式创建一个 WPF 应用程序。 ComboBoxes 用于过滤 ListView。我想要完成的是用相关 ListView 列中的项目填充组合框。换句话说,如果我的 ListView 有 Column1、Column2 和 Column3,我希望 ComboBox1 显示 Column1 中的所有 UNIQUE 项。一旦在 ComboBox1 中选择了一个项目,我希望根据 ComboBox1 的选择过滤 ComboBox2 和 ComboBox3 中的项目,这意味着 ComboBox2 和 ComboBox3 只能包含有效的选择。如果在 ASP.NET 中使用 AJAX 工具包,这将有点类似于 CascadingDropDown 控件,除了用户可以随机选择任何 ComboBox,而不是按顺序。

我的第一个想法是将 ComboBoxes 绑定到 ListView 绑定的同一个 ListCollectionView,并将 DisplayMemberPath 设置为适当的列。就将 ListView 和 ComboBoxes 一起过滤而言,这非常有效,但它会显示 ComboBox 中的所有项目,而不仅仅是唯一的项目(显然)。所以我的下一个想法是使用 ValueConverter 只返回唯一的项目,但我没有成功。

仅供参考:我在CodeProject 上阅读了 Colin Eberhardt 关于向 ListView 添加自动过滤器的帖子,但他的方法循环遍历整个 ListView 中的每个项目并将唯一的项目添加到集合中。虽然这种方法有效,但对于大型列表来说似乎会很慢。

关于如何优雅地实现这一目标的任何建议?谢谢!

代码示例:

<ListView ItemsSource="{Binding Products}" SelectedItem="{Binding SelectedProduct}">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Item" Width="100" DisplayMemberBinding="{Binding ProductName}"/>
            <GridViewColumn Header="Type" Width="100" DisplayMemberBinding="{Binding ProductType}"/>
            <GridViewColumn Header="Category" Width="100" DisplayMemberBinding="{Binding Category}"/>
        </GridView>
    </ListView.View>
</ListView>

<StackPanel Grid.Row="1">
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"/>
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="ProductType"/>
    <ComboBox ItemsSource="{Binding Products}" DisplayMemberPath="Category"/>
</StackPanel>

【问题讨论】:

  • 您能解释一下为什么使用 ValueConverter 对您不起作用吗?
  • 克里斯,在我的 ValueConverter 中,我尝试使用 LINQ 语句返回唯一项目,但我无法弄清楚如何查询 ListCollectionView 中的一列...我不确定如果可能的话。即使有可能,当在另一个 ComboBox 中进行选择时,ValueConverter 如何知道“刷新”列表?有什么想法吗?

标签: wpf listview mvvm combobox filter


【解决方案1】:

看看这个:

<Window x:Class="DistinctListCollectionView.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DistinctListCollectionView"
Title="Window1" Height="300" Width="300">
<Window.Resources>
    <local:PersonCollection x:Key="data">
        <local:Person FirstName="aaa" LastName="xxx" Age="1"/>
        <local:Person FirstName="aaa" LastName="yyy" Age="2"/>
        <local:Person FirstName="aaa" LastName="zzz" Age="1"/>
        <local:Person FirstName="bbb" LastName="xxx" Age="2"/>
        <local:Person FirstName="bbb" LastName="yyy" Age="1"/>
        <local:Person FirstName="bbb" LastName="kkk" Age="2"/>
        <local:Person FirstName="ccc" LastName="xxx" Age="1"/>
        <local:Person FirstName="ccc" LastName="yyy" Age="2"/>
        <local:Person FirstName="ccc" LastName="lll" Age="1"/>
    </local:PersonCollection>
    <local:PersonAutoFilterCollection x:Key="data2" SourceCollection="{StaticResource data}"/>
    <DataTemplate DataType="{x:Type local:Person}">
        <WrapPanel>
            <TextBlock Text="{Binding FirstName}" Margin="5"/>
            <TextBlock Text="{Binding LastName}" Margin="5"/>
            <TextBlock Text="{Binding Age}" Margin="5"/>
        </WrapPanel>
    </DataTemplate>
</Window.Resources>
<DockPanel>
    <WrapPanel DockPanel.Dock="Top">
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[0]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[1]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
        <ComboBox DataContext="{Binding Source={StaticResource data2}, Path=Filters[2]}" ItemsSource="{Binding DistinctValues}" SelectedItem="{Binding Value}" Width="120"/>
    </WrapPanel>
    <ListBox ItemsSource="{Binding Source={StaticResource data2}, Path=FilteredCollection}"/>
</DockPanel>
</Window>

还有视图模型:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.ComponentModel;

namespace DistinctListCollectionView
{
    class AutoFilterCollection<T> : INotifyPropertyChanged
    {
        List<AutoFilterColumn<T>> filters = new List<AutoFilterColumn<T>>();
        public List<AutoFilterColumn<T>> Filters { get { return filters; } }

        IEnumerable<T> sourceCollection;
        public IEnumerable<T> SourceCollection
        {
            get { return sourceCollection; }
            set
            {
                if (sourceCollection != value)
                {
                    sourceCollection = value;
                    CalculateFilters();
                }
            }
        }

        void CalculateFilters()
        {
            var propDescriptors = typeof(T).GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
            foreach (var p in propDescriptors)
            {
                Filters.Add(new AutoFilterColumn<T>()
                {
                    Parent = this,
                    Name = p.Name,
                    Value = null
                });
            }
        }

        public IEnumerable GetValuesForFilter(string name)
        {
            IEnumerable<T> result = SourceCollection;
            foreach (var flt in Filters)
            {
                if (flt.Name == name) continue;
                if (flt.Value == null || flt.Value.Equals("All")) continue;
                var pdd = typeof(T).GetProperty(flt.Name);
                {
                    var pd = pdd;
                    var fltt = flt;
                    result = result.Where(x => pd.GetValue(x, null).Equals(fltt.Value));
                }
            }
            var pdx = typeof(T).GetProperty(name);
            return result.Select(x => pdx.GetValue(x, null)).Concat(new List<object>() { "All" }).Distinct();
        }

        public AutoFilterColumn<T> GetFilter(string name)
        {
            return Filters.SingleOrDefault(x => x.Name == name);
        }

        public IEnumerable<T> FilteredCollection
        {
            get
            {
                IEnumerable<T> result = SourceCollection;
                foreach (var flt in Filters)
                {
                    if (flt.Value == null || flt.Value.Equals("All")) continue;
                    var pd = typeof(T).GetProperty(flt.Name);
                    {
                        var pdd = pd;
                        var fltt = flt;
                        result = result.Where(x => pdd.GetValue(x, null).Equals(fltt.Value));
                    }
                }
                return result;
            }
        }

        internal void NotifyAll()
        {
            foreach (var flt in Filters)
                flt.Notify();
            OnPropertyChanged("FilteredCollection");
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

        #endregion
    }

    class AutoFilterColumn<T> : INotifyPropertyChanged
    {
        public AutoFilterCollection<T> Parent { get; set; }
        public string Name { get; set; }
        object theValue = null;
        public object Value
        {
            get { return theValue; }
            set
            {
                if (theValue != value)
                {
                    theValue = value;
                    Parent.NotifyAll();
                }
            }
        }
        public IEnumerable DistinctValues
        {
            get
            {
                var rc = Parent.GetValuesForFilter(Name);
                return rc;
            }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string prop)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }

        #endregion

        internal void Notify()
        {
            OnPropertyChanged("DistinctValues");
        }
    }
}

其他类:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DistinctListCollectionView
{
    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DistinctListCollectionView
{
    class PersonCollection : List<Person>
    {
    }

    class PersonAutoFilterCollection : AutoFilterCollection<Person>
    {
    }
}

【讨论】:

  • Aviad,这很好用!我编译了你的源代码并且它工作了......我需要在我的应用程序中尝试它,但我相信它会工作。我只有一个问题。是否可以在 XAML 代码中将组合框的 DataContext 从:DataContext="{Binding Source={StaticResource data2}, Path=Filters[0]}" 更改为:DataContext="{Binding Source={StaticResource data2}, Path=Filters.FirstName}" 如果可能,我更喜欢使用名称,而不是数字。尽管如此,它确实有效,我非常感激,所以我会将其标记为已回答。感谢您的帮助!
  • 首先,很高兴您发现它有用,不客气。如果您在 AutoFilterCollection 类上实现 ICustomTypeDescriptor,则可以使用实际的列名。我刚刚做了它并且它有效,但这很乏味,因为您必须在 ICustomTypeDescriptor 提供的属性中包含标准属性(SourceCollection)以避免设计时错误(但没有编译错误或运行时错误)。
【解决方案2】:

如果您使用的是 MVVM,那么您的所有绑定数据对象都在您的 ViewModel 类中,并且您的 ViewModel 类正在实现 INotifyPropertyChanged,对吗?

如果是这样,那么您可以维护 SelectedItemType1、SelectedItemType2 等的状态变量,这些变量绑定到您的 ComboBox(es) SelectedItem 依赖项属性。在 SelectedItemType1 的 Setter 中,填充 List 属性(绑定到 ComboBoxType2 的 ItemsSource)并为 List 属性触发 NotifyPropertyChanged。对 Type3 重复此操作,您应该就可以了。

至于“刷新”问题,或者视图如何知道什么时候发生了变化,这一切都归结为绑定模式并在正确的时刻触发 NotifyPropertyChanged 事件。

您可以使用 ValueConverter 来做到这一点,我喜欢 ValueConverters,但我认为在这种情况下,管理您的 ViewModel 以便发生绑定更为优雅。

【讨论】:

  • 谢谢乔尔,虽然你的回答会奏效,但似乎需要大量代码才能使这项工作在各个方面都有效。例如,在 SelectedItemType1 的 Setter 中,我必须为所有其他 ComboBox 填充列表,并考虑到这些组合框可能已经有一个选定项目,这意味着我需要根据两个或多个选定项目填充一个列表.我添加的组合框越多,效果就越差。
  • 我喜欢使用一个包含多列的主列表并将组合框绑定到其中一列的想法。任何时候进行选择时,都会根据选择自动过滤其他组合框。然而,缺点是下拉列表显示所有项目,而不是唯一的项目。
  • 我认为如果您希望过滤它们,您将不得不为每个过滤器公开不同的属性。这些可以在 Get 方法中,并且会在 PropertyChanged 事件触发时被拉取。您仍然需要某种双向绑定来指示列表应如何过滤。我想另一个想法是尝试修改每个列表的 ItemsTemplate 以设置项目可见性。这对排序没有帮助,但可能是一种不同的过滤方式。只是另一个想法。
【解决方案3】:

为什么不使用 linq 查询或类似的方法创建另一个仅包含列表中不同值的属性?

public IEnumerable<string> ProductNameFilters
{
     get { return Products.Select(product => product.ProductName).Distinct(); }
}

...等等。

当您的 Product 属性更改时,您必须为每个过滤器列表发出属性更改通知,但这没什么大不了的。

您应该真正将您的 ViewModel 视为您视图的大型 ValueConverter。我会在 MVVM 中使用 ValueConverter 的唯一一次是当我需要将数据从非视图特定的数据类型更改为 视图特定的数据类型时。示例:对于大于 10 的值,文本需要为红色,对于小于 10 的值,文本需要为蓝色...蓝色和红色是特定于视图的类型,不应该是从 ViewModel 返回的东西。这确实是该逻辑不应该出现在 ViewModel 中的唯一情况。

我质疑“大型列表非常慢”评论的有效性......通常,人类的“大”和计算机的“大”是两个非常不同的东西。如果您处于计算机和人类的“大”领域,我也会质疑在屏幕上显示这么多数据。重点是,它可能还不足以让您注意到这些查询的成本。

【讨论】:

  • 谢谢安德森,但是如果我创建一个 ICollectionView 或 ListCollectionView 并过滤列表,如果我总是从 ObservableCollection 中选择不同的记录,我该如何更新 ProductNameFilters 属性?每当我过滤列表时,它仍然会使用您的方法返回所有记录。
  • Anderson,ListView 绑定到一个 ObservableCollection。然后,我创建一个 ICollectionView 来对 ListView 执行过滤、排序和分组等操作。使用您的方法,如果我创建一个属性来返回不同的值,那么当我过滤 ICollectionView 时,永远不会过滤这个 IEnumerable 列表。那有意义吗?换句话说,调用 myICollectionView.Filter = delgate(object obj){...};然后引发属性更改通知不会过滤 IEnumerable ProductNameFilters 属性。
  • 那是因为您需要更改 linq 表达式以针对过滤器结果集,而不是源 observable 集合。
  • 这就是我想做的,但我不知道怎么做。 ICollectionView 没有 Select 方法。你能帮帮我吗?
猜你喜欢
  • 2021-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-25
  • 1970-01-01
  • 2019-03-07
  • 1970-01-01
相关资源
最近更新 更多