Click here to Skip to main content
15,891,184 members
Articles / Desktop Programming / WPF

Developing an Autofilter ListView

Rate me:
Please Sign up or sign in to vote.
4.97/5 (26 votes)
8 Jul 2008CPOL7 min read 161.2K   5.4K   100  
This article describes the development of an Excel-like Autofilter ListView.
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows;
using System.Collections;
using System.ComponentModel;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using BaseWPFHelpers;

namespace ScottLogic.Controls
{
    /// <summary>
    /// Extends ListView to provide filterable columns
    /// </summary>
    public class FilterableListView : SortableListView
    {
        #region dependency properties

        /// <summary>
        /// The style applied to the filter button when it is an active state
        /// </summary>
        public Style FilterButtonActiveStyle
        {
            get { return (Style)GetValue(FilterButtonActiveStyleProperty); }
            set { SetValue(FilterButtonActiveStyleProperty, value); }
        }

        public static readonly DependencyProperty FilterButtonActiveStyleProperty =
                       DependencyProperty.Register("FilterButtonActiveStyle", typeof(Style), typeof(FilterableListView), new UIPropertyMetadata(null));

        /// <summary>
        /// The style applied to the filter button when it is an inactive state
        /// </summary>
        public Style FilterButtonInactiveStyle
        {
            get { return (Style)GetValue(FilterButtonInactiveStyleProperty); }
            set { SetValue(FilterButtonInactiveStyleProperty, value); }
        }

        public static readonly DependencyProperty FilterButtonInactiveStyleProperty =
                       DependencyProperty.Register("FilterButtonInActiveStyle", typeof(Style), typeof(FilterableListView), new UIPropertyMetadata(null));

        #endregion

        public static readonly ICommand ShowFilter = new RoutedCommand();

        private ArrayList filterList;
                
        #region inner classes

        /// <summary>
        /// A simple data holder for passing information regarding filter clicks
        /// </summary>
        struct FilterStruct
        {
            public Button button;
            public FilterItem value;
            public String property;

            public FilterStruct(String property, Button button, FilterItem value)
            {
                this.value = value;
                this.button = button;
                this.property = property;
            }
        }

        /// <summary>
        /// The items which are bound to the drop down filter list
        /// </summary>
        private class FilterItem : IComparable
        {
            /// <summary>
            /// The filter item instance
            /// </summary>
            private Object item;

            public Object Item
            {
                get { return item; }
                set { item = value; }
            }

            /// <summary>
            /// The item viewed in the filter drop down list. Typically this is the same as the item
            /// property, however if item is null, this has the value of "[empty]"
            /// </summary>
            private Object itemView;

            public Object ItemView
            {
                get { return itemView; }
                set { itemView = value; }
            }

            public FilterItem(IComparable item)
            {
                this.item = this.itemView = item;
                if (item == null)
                {
                    itemView = "[empty]";
                }
            }

            public override int GetHashCode()
            {
                return item != null ? item.GetHashCode() : 0;
            }

            public override bool Equals(object obj)
            {
                FilterItem otherItem = obj as FilterItem;
                if (otherItem != null)
                {
                    if (otherItem.Item == this.Item)
                    {
                        return true;
                    }
                }
                return false;
            }

            public int CompareTo(object obj)
            {
                FilterItem otherFilterItem = (FilterItem)obj;

                if (this.Item == null && obj == null)
                {
                    return 0;
                }
                else if (otherFilterItem.Item != null && this.Item != null)
                {
                    return ((IComparable)item).CompareTo((IComparable)otherFilterItem.item);
                }
                else
                {
                    return -1;
                }
            }

        }

        #endregion

        private Hashtable currentFilters = new Hashtable();

        private void AddFilter(String property, FilterItem value, Button button)
        {
            if (currentFilters.ContainsKey(property))
            {
                currentFilters.Remove(property);
            }
            currentFilters.Add(property, new FilterStruct(property, button, value));
        }

        private bool IsPropertyFiltered(String property)
        {
            foreach (String filterProperty in currentFilters.Keys)
            {
                FilterStruct filter = (FilterStruct)currentFilters[filterProperty];
                if (filter.property == property)
                    return true;
            }

            return false;
        }
        
               
        

        public FilterableListView()
        {
            CommandBindings.Add(new CommandBinding(ShowFilter, ShowFilterCommand));            
        }


        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            Uri uri = new Uri("/Controls/FiterListViewDictionary.xaml", UriKind.Relative);
            dictionary = Application.LoadComponent(uri) as ResourceDictionary;

            // cast the ListView's View to a GridView
            GridView gridView = this.View as GridView;
            if (gridView != null)
            {
                // apply the data template, that includes the popup, button etc ... to each column
                foreach (GridViewColumn gridViewColumn in gridView.Columns)
                {
                    gridViewColumn.HeaderTemplate = (DataTemplate)dictionary["FilterGridHeaderTemplate"];                    
                }
            }

        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // ensure that the custom inactive style is applied
            if (FilterButtonInactiveStyle != null)
            {
                List<FrameworkElement> columnHeaders = Helpers.FindElementsOfType(this, typeof(GridViewColumnHeader));

                foreach (FrameworkElement columnHeader in columnHeaders)
                {
                    Button button = (Button)Helpers.FindElementOfType(columnHeader, typeof(Button));
                    if (button != null)
                    {
                        button.Style = FilterButtonInactiveStyle;
                    }
                }
            }
            
        }


        /// <summary>
        /// Handles the ShowFilter command to populate the filter list and display the popup
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ShowFilterCommand(object sender, ExecutedRoutedEventArgs e)
        {
            Button button = e.OriginalSource as Button;

            if (button != null)
            {
                // navigate up to the header
                GridViewColumnHeader header = (GridViewColumnHeader)Helpers.FindElementOfTypeUp(button, typeof(GridViewColumnHeader));

                // then down to the popup
                Popup popup = (Popup)Helpers.FindElementOfType(header, typeof(Popup));

                if (popup != null)
                {
                    // find the property name that we are filtering
                    SortableGridViewColumn column = (SortableGridViewColumn)header.Column;
                    String propertyName = column.SortPropertyName;


                    // clear the previous filter
                    if (filterList == null)
                    {
                        filterList = new ArrayList();
                    }
                    filterList.Clear();

                    // if this property is currently being filtered, provide an option to clear the filter.
                    if (IsPropertyFiltered(propertyName))
                    {
                        filterList.Add(new FilterItem("[clear]"));
                    }
                    else
                    {   
                        bool containsNull = false;
                        PropertyDescriptor filterPropDesc = TypeDescriptor.GetProperties(typeof(Employee))[propertyName];

                        // iterate over all the objects in the list
                        foreach (Object item in Items)
                        {
                            object value = filterPropDesc.GetValue(item);
                            if (value != null)
                            {
                                FilterItem filterItem = new FilterItem(value as IComparable);
                                if (!filterList.Contains(filterItem))
                                {
                                    filterList.Add(filterItem);
                                }
                            }
                            else
                            {
                                containsNull = true;
                            }
                        }

                        filterList.Sort();

                        if (containsNull)
                        {
                            filterList.Add(new FilterItem(null));
                        }
                    }

                    // open the popup to display this list
                    popup.DataContext = filterList;
                    CollectionViewSource.GetDefaultView(filterList).Refresh();
                    popup.IsOpen = true;
                    
                    // connect to the selection change event
                    ListView listView = (ListView)popup.Child;
                    listView.SelectionChanged += SelectionChangedHandler;
                }
            }
        }

        /// <summary>
        /// Applies the current filter to the list which is being viewed
        /// </summary>
        private void ApplyCurrentFilters()
        {
            if (currentFilters.Count == 0)
            {
                Items.Filter = null;
                return;
            }
            
            // construct a filter and apply it               
            Items.Filter = delegate(object item)
            {
                // when applying the filter to each item, iterate over all of
                // the current filters
                bool match = true;
                foreach (FilterStruct filter in currentFilters.Values)
                {
                    // obtain the value for this property on the item under test
                    PropertyDescriptor filterPropDesc = TypeDescriptor.GetProperties(typeof(Employee))[filter.property];
                    object itemValue = filterPropDesc.GetValue((Employee)item);

                    if (itemValue != null)
                    {
                        // check to see if it meets our filter criteria
                        if (!itemValue.Equals(filter.value.Item))
                            match = false;
                    }
                    else
                    {
                        if (filter.value.Item != null)
                            match = false;
                    }
                }
                return match;
            };
        }

        /// <summary>
        /// Handles the selection change event from the filter popup
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SelectionChangedHandler(object sender, SelectionChangedEventArgs e)
        {
            // obtain the term to filter for
            ListView filterListView = (ListView)sender;
            FilterItem filterItem = (FilterItem)filterListView.SelectedItem;

            // navigate up to the header to obtain the filter property name
            GridViewColumnHeader header = (GridViewColumnHeader)Helpers.FindElementOfTypeUp(filterListView, typeof(GridViewColumnHeader));

            SortableGridViewColumn column = (SortableGridViewColumn)header.Column;
            String currentFilterProperty = column.SortPropertyName;

            if (filterItem == null)
                return;

            // determine whether to clear the filter for this column
            if (filterItem.ItemView.Equals("[clear]"))
            {
                if (currentFilters.ContainsKey(currentFilterProperty))
                {
                    FilterStruct filter = (FilterStruct)currentFilters[currentFilterProperty];
                    filter.button.ContentTemplate = (DataTemplate)dictionary["filterButtonInactiveTemplate"];
                    if (FilterButtonInactiveStyle != null)
                    {
                        filter.button.Style = FilterButtonInactiveStyle;
                    }
                    currentFilters.Remove(currentFilterProperty);
                }

                ApplyCurrentFilters();                
            }
            else
            {   
                // find the button and apply the active style
                Button button = (Button)Helpers.FindVisualElement(header, "filterButton");
                button.ContentTemplate = (DataTemplate)dictionary["filterButtonActiveTemplate"];

                if (FilterButtonActiveStyle != null)
                {
                    button.Style = FilterButtonActiveStyle;
                }

                AddFilter(currentFilterProperty, filterItem, button);
                ApplyCurrentFilters();
            }

            // navigate up to the popup and close it
            Popup popup = (Popup)Helpers.FindElementOfTypeUp(filterListView, typeof(Popup));
            popup.IsOpen = false;
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Scott Logic
United Kingdom United Kingdom
I am CTO at ShinobiControls, a team of iOS developers who are carefully crafting iOS charts, grids and controls for making your applications awesome.

I am a Technical Architect for Visiblox which have developed the world's fastest WPF / Silverlight and WP7 charts.

I am also a Technical Evangelist at Scott Logic, a provider of bespoke financial software and consultancy for the retail and investment banking, stockbroking, asset management and hedge fund communities.

Visit my blog - Colin Eberhardt's Adventures in .NET.

Follow me on Twitter - @ColinEberhardt

-

Comments and Discussions