65.9K
CodeProject is changing. Read more.
Home

How to make a sortable DataGrid for custom IBindable collections

starIconstarIconemptyStarIconemptyStarIconemptyStarIcon

2.00/5 (1 vote)

Mar 10, 2007

CPOL

3 min read

viewsIcon

22805

A sortable DataGrid for custom collections.

Introduction

It was a dark and stormy day... and my boss asked for the option to sort a DataGrid. If bound to a DataTable, the easy solution is to sort the DataView. Alas! my application uses LLBLGen Pro to build a Data Access Layer on top of the database. Initially, we weren't too keen to use LLBLGen, basically because we didn't get the full power of that application. But now, I was stuck with a bunch of custom collections on which no sort was provided. My colleague who wrote those parts even commented: don't use sort, sort the collection before binding.

Background

On CodeProject, I found an article that explained quite a lot. It however left me quite a lot puzzled too. Anyways, I had to start on it. Now that it's complete, I found it fit to share the entire set of code I needed to make it work.

Code

DataGrid Extension

I made a very simple extension of the DataGrid. In Visual Studio, right click on your project, select "Add", and pick "Component". You can copy the code below if the namespace is adjusted.

The code below is an adaption of the article above and the code mentioned in one of the comments.

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Reflection;

namespace Dipica.BM.Gui.Controls
{
    public partial class DataGridSortable : DataGrid
    {

        private bool m_columnHeaderClicked = false;
        public DataGridSortable(): base()
        {
            InitializeComponent();
        }

        public DataGridSortable(IContainer container): base()
        {
            container.Add(this);
            InitializeComponent();
        }

        /// <summary>
        /// Determine point where is clicked, and see if it's a header. 
        /// If so, please do sort!
        /// </summary>
        /// <param name="e"></param>

        protected override void OnMouseDown(MouseEventArgs e)
        {
            Point pt = new Point(e.X, e.Y);
            DataGrid.HitTestInfo hti = this.HitTest(pt);
            m_columnHeaderClicked = (hti.Type == HitTestType.ColumnHeader);
            if (m_columnHeaderClicked)
            {
                this.SortColumn(hti.Column);
                // invalidate and redraw to avoid weird results
                this.Refresh();
            }
            else
            {
                base.OnMouseDown(e);
            }
        }

        /// <summary>
        /// Sort method based on reflection and user added columns
        /// </summary>
        /// <param name="columnIndex"></param>
        private void SortColumn(int columnIndex)
        {
            MethodInfo mi = 
                typeof(CurrencyManager).GetMethod("SetSort", 
                BindingFlags.Instance | BindingFlags.NonPublic);
            CurrencyManager curMan = 
                (CurrencyManager)this.BindingContext[this.DataSource, 
                                                     this.DataMember];
            mi.Invoke(curMan, new object[] { 
                this.TableStyles[0].GridColumnStyles[columnIndex].PropertyDescriptor, 
                ListSortDirection.Ascending });
        }
    }
}

IBindable Collection

We use an abstract class called EntityCollectionBindingList that is used in all the collections, so I had to do my adaptations only once. It's the normal IBindingList implementation, so I've only included the parts that actually differ from the standard VS stuff.

The this.InnerList part in the code below is a reference to the ArrayList that holds the objects. My abstract EntityCollectionBindingList class extends the CollectionBase and the IBindingList.

The Comparer internal class is a custom part to sort the collection, it doesn't hold all types, but does quite a good job, as far as I'm concerned.

This code is based upon the code written by Doug Bell, I just took his comment to heart and placed it in an abstract class. That's why it's all combined below.

private PropertyDescriptor m_sortProperty;
private ListSortDirection m_sortDirection;
public virtual void ApplySort(PropertyDescriptor property, 
                              ListSortDirection direction)
{
    m_sortProperty = property;
    m_sortDirection = direction;
    this.InnerList.Sort(new Comparer(m_sortProperty, m_sortDirection));
} 

public virtual PropertyDescriptor SortProperty
{
    get { return m_sortProperty; }
}

public virtual bool IsSorted
{
    get { return (m_sortProperty != null); }
}

public virtual void RemoveSort()
{
    m_sortProperty = null;
}

public virtual ListSortDirection SortDirection
{
    get { return m_sortDirection; }
}

public virtual bool SupportsSorting
{
    get { return true; }
}

internal class Comparer : IComparer
{
    private PropertyDescriptor m_sortProperty;
    private ListSortDirection m_sortDirection;

    public Comparer(PropertyDescriptor sortProperty, 
                    ListSortDirection sortDirection)
    {
        this.m_sortProperty = sortProperty;
        this.m_sortDirection = sortDirection;
    }

    public int Compare(object x, object y)
    {
        Type propType = m_sortProperty.PropertyType;
        int sort = 0;

        if (propType == typeof(string)) 
            sort = string.Compare((string)m_sortProperty.GetValue(x), 
                   (string)m_sortProperty.GetValue(y));

        else if (propType == typeof(DateTime)) 
            sort = DateTime.Compare((DateTime)m_sortProperty.GetValue(x), 
                   (DateTime)m_sortProperty.GetValue(y));

        else if (propType.IsEnum) 
            sort = (int)m_sortProperty.GetValue(x) - (int)m_sortProperty.GetValue(y);

        else if (propType == typeof(int)) 
            sort = (int)m_sortProperty.GetValue(x) - 
                   (int)m_sortProperty.GetValue(y);

        else if (propType == typeof(Type)) 
            sort = string.Compare(m_sortProperty.GetValue(x).ToString(), 
                                  m_sortProperty.GetValue(y).ToString());

        else if (propType == typeof(bool)) 
            sort = string.Compare(m_sortProperty.GetValue(x).ToString(), 
                                  m_sortProperty.GetValue(y).ToString()); 

        else throw new NotSupportedException(); 

        if (m_sortDirection == ListSortDirection.Descending)
            sort = -sort; 
        return sort;
    }
}

The Combination

As I already had the grids, I simply updated their type to DataGridSortable in the code. I mapped our custom collection PersonDocumentCollection; it now can be sorted on any column. The code below is strictly for reference.

  • PersonDocumentCollection is a collection that extends the EntityCollectionBindingList, my abstract class as explained above.
  • Helpers.AddColumnStyle is code to make adding ColumnStyles easier. It's not included in this article.
  • Helpers.ValidateGrid is custom code to validate all columns used in the grid; if the MappingName doesn't exist in the objects in the collection, it gives an error. I added this code as I had to severely modify my database lately and non-existing rows in grids are simply not displayed. The code is not included in this article.
  • m_dataGridEDClosed is already declared as a private member of the DataGridSortable type.
DataGridTableStyle tableStyle = new DataGridTableStyle();
DataGridColumnStyle style;
PersonDocumentCollection m_personDocuments = new PersonDocumentCollection();
tableStyle.MappingName = "PersonDocumentCollection";
style = Helpers.AddColumnStyle(tableStyle, "Name", "Name", 
ColumnStyleType.Text, true);

style.Width = 250;

style = Helpers.AddColumnStyle(tableStyle, "DocumentType", "Type", 
ColumnStyleType.Text, true);

style.Width = 150;

Helpers.AddColumnStyle(tableStyle, "DateCreated", "Created", 
ColumnStyleType.DateTime, true);

Helpers.AddColumnStyle(tableStyle, "IsReceived", "Received?", 
ColumnStyleType.Boolean, false);

Helpers.AddColumnStyle(tableStyle, "DateReceived", "Received", 
ColumnStyleType.DateTime, false);

m_dataGridEDClosed.TableStyles.Add(tableStyle);

Helpers.ValidateGrid("PersonDocument", tableStyle, false, this.Name);

m_dataGridEDClosed.AllowNavigation = false;
m_dataGridEDOpen.AllowSorting = true;
m_dataGridEDClosed.ReadOnly = true;
m_personDocuments = new PersonDocumentCollection(pdColl);
m_dataGridEDOpen.SetDataBinding(m_personDocuments, null);

Conclusion

To be able to sort a custom collection in a DataGrid, you need a collection that extends the IBindable interface and a custom DataGrid that has the ability to react on ColumnHeader clicks. That's all there is to it.