WPF filter combobox items based on ListView items

10,289

Check this out:

<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>

And the view model:

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");
        }
    }
}

The other classes:

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>
    {
    }
}
Share:
10,289
Brent
Author by

Brent

Updated on June 04, 2022

Comments

  • Brent
    Brent almost 2 years

    I'm creating a WPF application using the MVVM design pattern that consists of a ListView and some ComboBoxes. The ComboBoxes are used to filter the ListView. What I am trying to accomplish is populating the combobox with items in the related ListView column. In other words, if my ListView has Column1, Column2, and Column3, I want ComboBox1 to display all UNIQUE items in Column1. Once an item is selected in the ComboBox1, I want the items in ComboBox2 and ComboBox3 to be filtered based on ComboBox1's selection, meaning that ComboBox2 and ComboBox3 can only contain valid selections. This would be somewhat similar to a CascadingDropDown control if using the AJAX toolkit in ASP.NET, except the user can select any ComboBox at random, not in order.

    My first thought was to bind ComboBoxes to the same ListCollectionView that the ListView is bound to, and set the DisplayMemberPath to the appropriate column. This works great as far as filtering the ListView and ComboBoxes together goes, but it displays all items in the ComboBox rather than just the unique ones (obviously). So my next thought was to use a ValueConverter to only return the only the unique items, but I have not been sucessful.

    FYI: I read Colin Eberhardt's post on adding a AutoFilter to a ListView on CodeProject, but his method loops through each item in the entire ListView and adds the unique ones to a collection. Although this method works, it seems that it would be very slow for large lists.

    Any suggestions on how to achieve this elegantly? Thanks!

    Code Example:

    <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>