WPF Using multiple filters on the same ListCollectionView

16.7k views Asked by At

I'm using the MVVM design pattern, with a ListView bound to a ListCollectionView on the ViewModel. I also have several comboboxes that are used to filter the ListView. When the user selects an item from the combobox, the ListView is filtered for the selected item. Whenever I want to filter on top of what is already filtered, it undoes my previous filter like it never happened. The same is also true for removing a filter. Removing a filter for one combobox removes all filters and displays the original list. Is it possible to have multiple, separate filters on the same ListCollectionView?

Am I doing something wrong, or is this simply not supported? You can find a screen capture of my application here to see what I am trying to accomplish. Here's my code for filtering...

    /// <summary>
    /// Filter the list
    /// </summary>
    /// <param name="filter">Criteria and Item to filter the list</param>
    [MediatorMessageSink("FilterList", ParameterType = typeof(FilterItem))]
    public void FilterList(FilterItem filter)
    {
        // Make sure the list can be filtered...
        if (Products.CanFilter)
        {
            // Now filter the list
            Products.Filter = delegate(object obj)
            {
                Product product = obj as Product;

                // Make sure there is an object
                if (product != null)
                {
                    bool isFiltered = false;
                    switch (filter.FilterItemName)
                    {
                        case "Category":
                            isFiltered = (product.Category.IndexOf(filter.Criteria, StringComparison.CurrentCultureIgnoreCase)) != -1 ? true : false;
                            break;

                        case "ClothingType":
                            isFiltered = (product.ClothingType.IndexOf(filter.Criteria, StringComparison.CurrentCultureIgnoreCase)) != -1 ? true : false;
                            break;

                        case "ProductName":
                            isFiltered = (product.ProductName.IndexOf(filter.Criteria, StringComparison.CurrentCultureIgnoreCase)) != -1 ? true : false;
                            break;

                        default:
                            break;
                    }

                    return isFiltered;
                }
                else
                    return false;
            };
        }
    }
2

There are 2 answers

0
Anvaka On

Every time you set Filter property you reset previous filter. This is a fact. Now how can you have multiple filters?

As you know, there are two ways to do filtering: CollectionView and CollectionViewSource. In the first case with CollectionView we filter with delegate, and to do multiple filters I'd create a class to aggregate custom filters and then call them one by one for each filter item. Like in the following code:

  public class GroupFilter
  {
    private List<Predicate<object>> _filters;

    public Predicate<object> Filter {get; private set;}

    public GroupFilter()
    {
      _filters = new List<Predicate<object>>();
      Filter = InternalFilter;
    }

    private bool InternalFilter(object o)
    {
      foreach(var filter in _filters)
      {
        if (!filter(o))
        {
          return false;
        }
      }

      return true;
    }

    public void AddFilter(Predicate<object> filter)
    {
      _filters.Add(filter);
    }

    public void RemoveFilter(Predicate<object> filter)
    {
      if (_filters.Contains(filter))
      {
        _filters.Remove(filter);
      }
    }    
  }

  // Somewhere later:
  GroupFilter gf = new GroupFilter();
  gf.AddFilter(filter1);
  listCollectionView.Filter = gf.Filter;

To refresh filtered view you can make a call to ListCollectionView.Refresh() method.

And in the second case with CollectionViewSource you use Filter event to filter collection. You can create multiple event handlers to filter by different criteria. To read more about this approach check this wonderful article by Bea Stollnitz: How do I apply more than one filter? (archive)

Hope this helps.

Cheers, Anvaka.

0
Kent Boogaart On

Every time the user filters, your code is replacing the Filter delegate in your collection view with a new, fresh one. Moreover, the new one only checks the particular criteria the user just selected with a ComboBox.

What you want is a single filter handler that checks all criteria. Something like:

public MyViewModel()
{
    products = new ObservableCollection<Product>();
    productsView = new ListCollectionView(products);
    productsView.Filter += FilterProduct;
}

public Item SelectedItem
{
    //get,set omitted. set needs to invalidate filter with refresh call
}

public Type SelectedType
{
    //get,set omitted. set needs to invalidate filter with refresh call
}

public Category SelectedCategory
{
    //get,set omitted. set needs to invalidate filter with refresh call
}

public ICollection<Product> FilteredProducts
{
    get { return productsView; }
}

private bool FilterProduct(object o)
{
    var product = o as Product;

    if (product == null)
    {
        return false;
    }

    if (SelectedItem != null)
    {
        // filter according to selected item
    }

    if (SelectedType != null)
    {
        // filter according to selected type
    }

    if (SelectedCategory != null)
    {
        // filter according to selected category
    }

    return true;
}

Your view can now just bind to the appropriate properties and the filtering will just work.