Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Outlook Style Grouped List Control

0.00/5 (No votes)
16 Aug 2007 64  
An Outlook Style List Control
Screenshot - ArticleImage.gif

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; // Change this if you want to increase 

                                  // the number of items in the list

    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.

Screenshot - SectionsDiagram.png

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):

Screenshot - FGScreenShot.jpg

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 
/// <summary />

/// Storage area for the row override.

/// </summary />

private RowOverrideExample _rowOverride;

private void toggleRowPaintingOverrideToolStripMenuItem_Click( object sender, 
    EventArgs e )
{
    if( _rowOverride == null )
    {
        //

        // Start overrride.

        _rowOverride = new RowOverrideExample( _superList );
    }
    else
    {
        //

        // Clear override.

        _rowOverride.Dispose();
        _rowOverride = null;
    }
}
/// <summary />

/// Example of overriding rows giving a gradient fill look.

/// </summary />

private class RowOverrideExample: IDisposable
{
    public RowOverrideExample( 
        BinaryComponents.SuperList.ListControl listControl )
    {
        _oldFactory = listControl.SectionFactory; // store old factory as we

                                                 // want to leave as we came.

        _listControl = listControl;

        //

        // Replace the current SectionFactory with our override.

        listControl.SectionFactory = new MySectionFactory(); // 

        _listControl.LayoutSections();
    }
    public void Dispose()
    {
        if( _oldFactory != null ) // put things back as they were

        {
            _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:

Screenshot - ToolStripOptionsToolbarSection.png

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here