Introduction
I've been a member of CodeProject pretty much since its inception in one form or another and have been meaning to post an article for a while, this is a great site and I'm glad that I can finally give something back.
I built the Superlist control whilst developing an RSS reader called FeedGhost. Although there are plenty of commercial grouped list controls available I wanted to have total control over the code and of course its usability. Superlist supports drag drop column customisation, grouping as well as handling thousands of entries smoothly. It's also highly customisable if you want to change its look and feel. In this article I'll explain how to use and extend the control in a demo project. If you download the source, you can find demo project under the Tests/SuperListTest directory.
Background
Before deciding to develop my own list control I spent a couple of weeks fighting the standard Listview, trying to get it to work the way I wanted, but I finally gave up when I couldn't get the selected items in the grouped visual order. We needed this in FeedGhost so that articles that the user multi-selected would be displayed in the same order in the corresponding HTML view.
I decided to write the control from scratch rather than basing it for example on the Grid controls. In previous projects I've worked on, I've seen the grids bent and warped into doing the bidding of its creator only to be a maintenance millstone around the projects neck later; not quite working the way you want with plenty of patching code to get it *nearly* there. In the end you have a pile of brittle code that people are too scared to touch.
I developed Superlist in two weeks, strangley enough it was about the same time I spent trying to get the Listview working in the first place, admittedly I spent the following two weeks fixing the bugs
Using the Code
The first point to note is I haven't done any forms designer compatibility work with this control. I tend to use the designer for laying the controls out and then go straight into the code to complete the rest of the work, hence no designer support for Superlist in terms of adding Columns and configuring them etc. To use the control you'll need of course to add it to a Form
or UserControl
, then in code you will need to create its columns as in the example below:
public SuperListTestForm()
{
InitializeComponent();
Column surnameColumn = new Column( "surname", "Surname", 120,
delegate( object item ) { return ((Person)item).Surname; } );
Column firstnameColumn = new Column( "firstname", "Firstname", 120,
delegate( object item ) { return ((Person)item).Firstname; } );
Column phoneColumn = new Column( "phone", "Phone", 100, delegate(
object item ) { return ((Person)item).Phone; } );
Column cityColumn = new Column( "city", "City", 60, delegate(
object item ) { return ((Person)item).City; } );
Column stateColumn = new Column( "state", "State", 70, delegate(
object item ) { return ((Person)item).State; } );
Column dateColumn = new Column( "date", "Date", 110, delegate(
object item ) { return ((Person)item).Date.ToString(); } );
dateColumn.GroupItemAccessor = new ColumnItemValueAccessor(
GroupValueFromItem );
dateColumn.MoveBehaviour = Column.MoveToGroupBehaviour.Copy;
dateColumn.GroupSortOrder = SortOrder.Descending;
surnameColumn.SortOrder = SortOrder.Ascending;
_superList.Columns.Add( firstnameColumn );
_superList.Columns.Add( phoneColumn );
_superList.Columns.Add( stateColumn );
_superList.Columns.Add( cityColumn );
_superList.Columns.Add( dateColumn );
_superList.Columns.GroupedItems.Add( dateColumn );
_superList.Columns.GroupedItems.Add( stateColumn );
_superList.SelectedItems.DataChanged +=
new SelectedItemsCollection.DataChangedHandler(
SelectedItems_DataChanged );
int tickStart = Environment.TickCount;
const int iterationCount = 1;
for( int i = 0; i < iterationCount; i++ )
{
_superList.Items.AddRange( Person.GetData() );
}
}
Column Object
The column object is where most of your work lays in terms of getting the control up and running, you create it with the following constructor:
public Column( string name, string caption, int width,
ColumnItemValueAccessor columnItemValueAccessor )
The name
parameter is used to uniquely identify the Column
for serialisation etc. The columnItemValueAccessor
parameter is a delegate you need to supply that is used to return back the object to render in the associated cell (normally a string):
public delegate object ColumnItemValueAccessor( object rowItem );
Once you've defined your columns you can add them to the list via the Columns
property.
Grouping
Grouping is as simple as adding the column to the Columns.GroupedItems
property. By default the value used to group on is the same as the columnItemValueAccessor
parameter passed in the Column
constructor. However, you can override this by supplying a new ColumnItemValueAccessor
delegate to the Column.GroupItemAccessor
property. In my example program I override this property to supply the grouped Date column values 'Today', 'Yesterday', 'Last Week' etc.
Sorting
The value returned by the columnItemValueAccessor
parameter by default must support IComparable
otherwise an exception will be thrown when sorting is applied. Alternatively you can override Column.Comparitor
and Column.GroupComparitor
if you want to handle comparisons manually. You set the initial sorting style of a Column
by setting the Column.SortOrder
property. There is also a Column.GroupSortOrder
property for setting the group ordering when in grouped mode.
Behaviour
In the past one of the performance bottelnecks that I've seen is where an application will add lots of items to a control causing it to visually slow down, as the control updates itself each time a new item is added. These problems are easy to fix and normally involve telling the control to disable rendering whilst the adding operation is in progress. We get around these potential problems with the Superlist as it processes changes to the list in the background, if you want the UI to syncronise with the changes straight away then you can call ListControl.Items.SynchroniseWithUINow()
. By default the background processing is done on application idle, you can change this so part of the processing is done in a separate thread by setting ListControl.Items.ProcessingStyle = BinaryComponents.SuperList.ItemLists.ProcessingStyle.Thread
, this will move the sorting part of processing over to a thread. Bare in mind that any of the properties on the Column object maybe called in the separate thread when in threading mode is set.
Design
Each visual aspect of the control like the header, rows and cells are all derived from Section
, a Section
is semantically similar to Control
but without the latter's resource overhead. It has a rectangular area, focus, mouse, and drag drop support.
The Sections
are contained by the SectionContainer
object which passes keyboard, mouse and drag drop information to them. In the case of the Superlist the ListControl
(not in the diagram above) derives from SectionContainerControl
, when the ListControl
is constructed it adds the CustomiseListSection
and the ListSection
to its canvas. The CustomiseListSection
contains the grouping columns as well as a ToolStrip for the lists commands. The ListSection
contains the list header, groups and rows.
Customising
The ListControl
exposes the property ListControl.SectionFactory
, when set with your own SectionFactory
you can override any of the lists Sections
. We use this in FeedGhost to give the List a glossy look (click on image for a larger pic):
When overriding a section the two main methods you'll be interested in will be void Section.Layout( GraphicsSettings gs, Size maximumSize )
and void Section.Paint( GraphicsSettings gs, Rectangle clipRect )
. Layout is called when the parent Section
wants to know the size and height of your Section
. To see an overriden row in my demo if you click on the 'Customize' menu item followed by 'Toggle Row Paint Override' you can see rows that are gradient filled. The code can be seen below taken from the demo applications Form
:
#region Example of overriding rows
private RowOverrideExample _rowOverride;
private void toggleRowPaintingOverrideToolStripMenuItem_Click( object sender,
EventArgs e )
{
if( _rowOverride == null )
{
_rowOverride = new RowOverrideExample( _superList );
}
else
{
_rowOverride.Dispose();
_rowOverride = null;
}
}
private class RowOverrideExample: IDisposable
{
public RowOverrideExample(
BinaryComponents.SuperList.ListControl listControl )
{
_oldFactory = listControl.SectionFactory;
_listControl = listControl;
listControl.SectionFactory = new MySectionFactory();
_listControl.LayoutSections();
}
public void Dispose()
{
if( _oldFactory != null )
{
_listControl.SectionFactory = _oldFactory;
_listControl.LayoutSections();
}
}
private class MySectionFactory : SectionFactory
{
public override RowSection CreateRowSection(
BinaryComponents.SuperList.ListControl listControl,
RowIdentifier rowIdenifier,
HeaderSection headerSection,
int position )
{
return new MyRowSection( listControl, rowIdenifier,
headerSection, position );
}
}
private class MyRowSection : RowSection
{
public MyRowSection(
BinaryComponents.SuperList.ListControl listControl,
RowIdentifier rowIdentifier,
HeaderSection headerSection,
int position )
: base( listControl, rowIdentifier, headerSection, position )
{
_position = position;
}
public override void PaintBackground( Section.GraphicsSettings gs,
Rectangle clipRect )
{
Color from, to;
if( _position % 2 == 0 )
{
from = Color.White;
to = Color.LightBlue;
}
else
{
to = Color.White;
from = Color.LightBlue;
}
using( LinearGradientBrush lgb = new LinearGradientBrush(
this.Rectangle,
from,
to,
LinearGradientMode.Horizontal ) )
{
gs.Graphics.FillRectangle( lgb, this.Rectangle );
}
}
public override void Paint( Section.GraphicsSettings gs,
Rectangle clipRect )
{
base.Paint( gs, clipRect );
}
private int _position;
}
private BinaryComponents.SuperList.ListControl _listControl;
private SectionFactory _oldFactory;
}
#endregion
On the customisation Section
there is a ToolStrip
that is created inside the ToolStripOptionsToolbarSection
object:
You can also add your own ToolStripItems to it by overriding the ToolStripOptionsToolbarSection
class and passing your version in via your own SectionFactory
. Alternatively you could replace the whole area if you wanted by deriving from OptionsToolbarSection
; we do this in FeedGhost using our own Ribbon strip instead.
Loading and Saving State
Superlist's state can be saved and loaded with void ListControl.SerializeState( System.IO.TextWriter writer )
and void ListControl.DeSerializeState( System.IO.TextReader reader )
respectively.
Saving Example
using( System.IO.TextWriter textWriter = File.CreateText( ofd.FileName ) )
{
_superList.SerializeState( textWriter );
}
Loading Example
using( System.IO.TextReader textReader = File.OpenText( fileName ) )
{
_superList.DeSerializeState( textReader );
}
Wrapping it Up
I hope you find this control useful and if you have any ideas, bugs or suggestions feel free leave a message here. I'll also be posting updates here as well as on my company site Binary Components.