Project Description

Jib.Grid is a reference implementation demonstrating how to add Grouping, Freezing, and Filtering to the base WPF DataGrid. This project can serve as the basis for your specific needs.
jibgrid_wpf.png
This project is intended to demonstrate how each of these features can be implemented using the standard DataGrid provided in WPF. In total, the solution consists of several hundred lines of code {excluding xaml :) }. This small code-base should help others in understanding the techniques used for each feature. While fully functional, this project should serve as a reference implementation not a final solution. In several edge cases simplicity was chosen over completeness. As such, there are several limitations to this control. Before you use the control, please review these limitations outlined below. As a side note, all these limitations can be resolved, but the solutions would add too much complexity and code for this projects intended purpose.


Filtering

The filter headers are added to the DataGrid by creating a custom ColumnHeaderStyle. This implementation has a serious drawback. To make a long story short, there is no easy way of tying the filter control to the corresponding column. In the end I use the VisualTreeHelper to find DataGridColumnHeader of the created filter control. From the DataGridColumnHeader I then find the column for the filter control.


private void ColumnFilterHeader_Loaded(object sender, RoutedEventArgs e)
        {
            DataGridColumn column = null;
            DataGridColumnHeader colHeader = null;

            UIElement parent = (UIElement)VisualTreeHelper.GetParent(this);
            while (parent != null)
            {
                parent = (UIElement)VisualTreeHelper.GetParent(parent);
                if (colHeader == null)
                    colHeader = parent as DataGridColumnHeader;

                if (Grid == null)
                    Grid = parent as JibGrid;
            }

            if (colHeader != null)
                column =  colHeader.Column;
        // the code continues...
            }

        }

Since the ItemSource for the grid is a CollectionViewSource, filtering is done by generating a predicate composed of all the filters entered by the user. This predicate is composed by each filter control dynamically creating a lamda expression and then having the grid AND each predicate into the final predicate for the CollectionViewSource's filter. For simplicity’s sake the lamda expression can only be generated off of first level properties. In addition to this, the filter controls do not support String Formatting (could not figure out how to apply it). It does support converters.


 void filter_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "FilterChanged")
            {
                Predicate<object> predicate = null;
                foreach (var filter in Filters)
                    if (filter.HasPredicate)
                        if (predicate == null)
                            predicate = filter.GeneratePredicate();
                        else
                            predicate = predicate.And(filter.GeneratePredicate());
                bool canContinue = true;
                var args = new CancelableFilterChangedEventArgs(predicate);
                if (BeforeFilterChanged != null && !IsResetting)
                {
                    BeforeFilterChanged(this, args);
                    canContinue = !args.Cancel;
                }
                if (canContinue)
                {
                    ListCollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource) as ListCollectionView;
                    if (view != null && view.IsEditingItem)
                        view.CommitEdit();
                    if (view != null && view.IsAddingNew)
                        view.CommitNew();
                    if (CollectionView != null)
                        CollectionView.Filter = predicate;
                    if (AfterFilterChanged != null)
                        AfterFilterChanged(this, new FilterChangedEventArgs(predicate));
                }
                else
                {
                    IsResetting = true;
                    var ctrl = sender as ColumnFilterControl;
                    ctrl.ResetControl();
                    IsResetting = false;
                }
            }
        }

Grouping

JibGrid uses the standard WPF technique of defining a row GroupStyle. If you are familiar with this approach, you will know that it has SEVER performance limitations. It seems that when a row style is defined, the grid no longer can perform row virtualization. As such, while you can group, it is a real performance killer.


public void AddGroup(string boundPropertyName)
        {
            if (!string.IsNullOrWhiteSpace(boundPropertyName) && CollectionView != null && CollectionView.GroupDescriptions != null)
            {
                foreach (var groupedCol in CollectionView.GroupDescriptions)
                {
                    var propertyGroup = groupedCol as PropertyGroupDescription;

                    if (propertyGroup != null && propertyGroup.PropertyName == boundPropertyName)
                        return;
                }

                CollectionView.GroupDescriptions.Add(new PropertyGroupDescription(boundPropertyName));
            }
        }

Freezing

The WPF DataGrid supports Freezing Columns. This is done by the grouping controls setting the FrozenColumnCount on the DataGrid. When the user selects a column to freeze, the grid changes the display index and increments the FrozenColumnCount.


        public void FreezeColumn(DataGridColumn column)
        {
            if (this.Columns != null && this.Columns.Contains(column))
            {
                column.DisplayIndex = this.FrozenColumnCount;
                this.FrozenColumnCount++;
            }
        }
        public bool IsFrozenColumn(DataGridColumn column)
        {
            if (this.Columns != null && this.Columns.Contains(column))
            {
                return column.DisplayIndex < this.FrozenColumnCount;
            }
            else
            {
                return false;
            }
        }
        public void UnFreezeColumn(DataGridColumn column)
        {
            if (this.FrozenColumnCount > 0 && column.IsFrozen && this.Columns != null && this.Columns.Contains(column))
            {
                this.FrozenColumnCount--;
                column.DisplayIndex = this.FrozenColumnCount;
            }
        }

Limitations

  1. You can group at your own risk :)
  2. The grid does not support property paths with a depth greater than one. ie {Binding Path=Contact.Address.City}
  3. While the grid supports converters, it does not support StringFormating

Last edited Feb 7, 2012 at 3:47 AM by Byrnesama, version 4