How to make a sortable DataGrid for custom IBindable collections





2.00/5 (1 vote)
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 theEntityCollectionBindingList
, my abstract class as explained above.Helpers.AddColumnStyle
is code to make addingColumnStyle
s easier. It's not included in this article.Helpers.ValidateGrid
is custom code to validate all columns used in the grid; if theMappingName
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 aprivate
member of theDataGridSortable
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.