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

XPTable: .NET ListView Update

0.00/5 (No votes)
4 Jul 2007 963  
An update to the excellent XPTable control

Screenshot - XPTableUpdate.png

Introduction

XPTable is a customizable ListView written by Matthew Hall. Go here to see the excellent article on XPTable and to download the original source code and demo application. After using it myself and being jolly impressed with it, I thought there were still some features that would be great to have. So, I have added some extras onto the original. This article explains what features have been added and how to use them.

PS: I don't know if anyone else has made any significant additions to XPTable. If they have, it might be worth sharing them. There is an XPTable project on SourceForge. Matthew Hall seems not to be actively involved with this control anymore.

After a brief description of the new features, there is a section for each feature that describes in more detail what it allows you to do, how you code it, and a summary of the changes I have made to XPTable to get it working. I have tried to keep these additions in the same style and following the same architecture as the original version. So hopefully if you are familiar with XPTable these will seem pretty intuitive.

Disclaimer

I realise that XPTable has now been around for a while and I am obviously not aware of all the various ways people will have used it. I may have introduced changes that break your existing usage of XPTable. If so, let me know; there may be a way around it.

Summary of new features

There are four new features now supported. Here they are:

  • Column spanning: just like Colspan in HTML
  • Word wrapping: the row height is increased so that all the text in a given cell can be wrapped and shown
  • Grouping of rows: this allows you to always keep rows together, like if you have Autopreview switched on for a mailbox in Outlook
  • Multiple sort indices

Column spanning

If you know HTML, you may well have used the <td colspan=2> attribute to allow the text in one column to flow over into the next column(s). Cells in XPTable can now similarly be allowed to span over a number of following cells.

Screenshot - XPTableUpdateColspan.png

Using the code

Quite simply, a cell now has the property ColSpan, which behaves just the same way as the colspan attribute in HTML. It has a default value of 1 and setting it to something greater than 1 means that the contents of that cell will be allowed to 'spill over' into the next ColSpan - 1 cells if there are that many. This property only affects a single cell. All the other cells in other rows will behave as normal, unless you set their ColSpan too.

Cell cell = new Cell("This is text that will go over to the next column");
cell.ColSpan = 2;
    // The contents of this cell will 'spill over' into the next cell.

Note that you do not add cell objects for the cells that are "covered over" by a colspanning cell. So if you have a table with 4 columns and you want to add a row where the second column spans over 2 columns, you only need to add 3 cells to the row as follows:

Row row = new Row();
row.Cells.Add(new Cell("column 1"));     // First cell is for column 1

Cell cell = new Cell("columns 2 and 3"); // Second cell is for column 2 and 3

cell.ColSpan = 2;
row.Cells.Add(cell)
// We don't actually add a cell for column 3 on its own

row.Cells.Add(new Cell("column 4"));
    // The third cell we add is actually for column 4

Implementation

Whenever a row is rendered, each cell checks to see if has ColSpan > 1. If so, it extends the size it renders over to include the following cells as appropriate. For the purposes of allowing selection and focus, the conversion from screen coordinatess (X, Y pixels) to grid coordinates (row, column) has to take this overlap into account.

Word wrapping

Cells can have word wrapping enabled, so that the text in a cell wraps and the height of the row is increased such that all of the text is visible.

Screenshot - XPTableUpdateWordWrap.png

Using the code

Enabling word wrapping means that each row may have a different height. This possibility introduces many more calculations when rending the table. It requires many calls to Graphics.MeasureString, so if it is not required it is best to switch it off globally. It is switched off by default, so if you want to use this, you need to enable it using Table.EnableWordWrap. With that enabled, simply set the property Cell.WordWrap to true for any cell you wish to word wrap.

private void AddRowsToTable()
{
    Table table = this.table;
        // The Table control on a form - already initialised

    table.EnableWordWrap = true;
        // If false, then Cell.WordWrap is ignored


    Row row1 = new Row();
    Cell cell1 = new Cell("This is a cell with quite long text");
    cell1.WordWrap = true;
        // The row height will be increased so we can see all the text

    row1.Cells.Add(cell1);

    Row row2 = new Row();
    Cell cell2 = new Cell("This is long text that will just be truncated");
    cell2.WordWrap = false;         // Not needed - it is false by default

    row2.Cells.Add(cell2);
}

Implementation

In Table.OnPaintRows(), if the global switch is enabled, then each row is checked to see if it contains a "word wrap" cell. If it does, then the renderer for that cell is obtained. Using GetCellHeight -- a new member of ICellRender -- the minimum height required to display the whole cell content is determined. This is then used as the new height for the row.

Rows now remember the height they were when they were last rendered, which is used in loads of places where anything to do with y-coordinates is required. Only TextCellRenderer actually returns anything at the moment. All other kinds of cell just go with the table default for row height. I'm not sure how useful or possible it is to wrap any other cell types.

Row grouping

If you use Outlook, you may have noticed that in the "email listview" you can switch on Auto Preview. This adds a row underneath each email showing a bit of the email content. However, if you sort the list you will also notice that the Auto Preview row ignores the sorting process. It just sticks with its "parent" row, which does obey the sorting. This is what I was after here. Yes, I know Outlook doesn't really do it with a listview, but I was after a similar effect.

You can now attach child rows to a parent row, so that the child rows just kind of "stick" to the parent when sorted. They remain under the parent row and are kept in the order they are added to the parent. This may not be what you were thinking of when you saw "XPTable does Grouping" and if so, sorry for the disappointment: this isn't the type of grouping that allows groups to be collapsed and expanded. These "Grouped" rows look like normal rows. They just behave differently when being sorted.

Screenshot - XPTableUpdate.png

Using the code

A Row now has a SubRows property, which is a RowCollection just like the Table.Rows property. To add a sub-row, the Row is just added to the SubRows collection and not to Table.Rows. The rest is as normal:

private void AddEmailRows(TableModel table, bool read, string from,
    string sent, string subject, string preview)
{
    Row row = new Row();            // This is the parent row

    row.Cells.Add(new Cell(
        "", read ? Resources.EmailRead : Resources.EmailUnRead));
    row.Cells.Add(new Cell(from));
    row.Cells.Add(new Cell(DateTime.Parse(sent)));
    table.Rows.Add(row);

    // Add a sub-row that shows just the

    // email subject in grey (single line only)

    Row subrow = new Row();         // The subject line is a sub-row

    subrow.Cells.Add(new Cell());   // Add cells to the subrow as normal

    Cell cell = new Cell(subject);
    cell.ForeColor = Color.Gray;
    cell.ColSpan = 2;
    subrow.Cells.Add(cell);
    row.SubRows.Add(subrow);        // Add this subrow to the parent row


    // The subrow is not added directly to the

    // main table - just the parent row


    // Add a sub-row that shows just a preview of the

    // email body in blue, and wraps too

    subrow = new Row();             // The preview line is the second sub-row

    subrow.Cells.Add(new Cell());   // Add cells to the subrow as normal

    cell = new Cell(preview);
    cell.ForeColor = Color.Blue;
    cell.ColSpan = 2;
    cell.WordWrap = true;
    subrow.Cells.Add(cell);
    row.SubRows.Add(subrow);        // Add this subrow to the parent row

}

Implementation

When a sub-row is added to the SubRows collection, it is in fact added to the main table's collection of Rows behind the scenes. However, it remains in the SubRows collection too. All the main behaviour of the row -- i.e. being rendered, clickable, everything other than sorting -- is done as for normal rows because it is in the main Rows collection.

The only time the SubRows collection comes into play is when sorting takes place. The sub-rows stick to the parent row when sorting occurs by adopting the parent values when being compared to other rows. That is, unless the row is being compared to another child of the same parent or the parent itself. In that case, the index in the SubRows collection is used in the comparison. In this way, the sub-rows always appear underneath the parent row in the order they were added to the SubRows collection.

Multiple sort indices

The original version allowed the table to be sorted using a single column as the sort key, activated by clicking on the column header. This primary sort column still behaves exactly as sorting did before, but you can now specify programatically a number of columns to be used as sort keys, along with the direction to sort in for each column. These take effect when two or more rows end up having the same value in the column that is being used as the primary sort key.

So, if you have 3 columns -- i.e. Firstname, Surname and Height -- you can now specify the sort order as, in pseudo SQL: ORDER BY Surname, Firstname, Height DESC. Now if the user clicks on Surname, the list is sorted first by Surname and then by Firstname. If two people have exactly the same name, then the tallest is at the top. I don't even know what the original XPTable would do in this scenario; just output them in the order they were added to the table?

In the example below, two tables have been created. The only difference between them is that the second has multiple sort columns defined: Surname, Firstname, Height DESC. The Surname column has been clicked on both tables and the resulting sorted table is shown in the images. Without multi-column sorting, the 3 Hobbs rows end up with the Marks above Dave, and then the shortest Mark at the top. This would appear to just be the order in which they were added to the table.

When multiple sorting columns are defined, the Hobbs rows have Dave first -- correctly sorted to be above the Marks -- and the Marks are correctly sorted so that the taller one is above the shorter. Of course, when the value in the primary sorting column is unique, the extra sorting columns have no effect.

Without multi-column sorting:

Screenshot - XPTableUpdateWithOutMulti.png

With multi-column sorting:

Screenshot - XPTableUpdateWithMulti.png

Using the code

Create a SortColumnCollection and add SortColumns to it as required. Then set this collection as the ColumnModel.SecondarySortOrders property.

// Order will be Surname, Name, Height DESC

SortColumnCollection sort = new SortColumnCollection();
sort.Add(new SortColumn(3, SortOrder.Ascending));   // Surname

sort.Add(new SortColumn(2, SortOrder.Ascending));   // Name

sort.Add(new SortColumn(1, SortOrder.Descending));  // Height

table.ColumnModel.SecondarySortOrders = sort;

Implementation

The sorting classes have been refactored into Row comparison operations and Cell comparisons. The SorterBase class manages the row comparisons and will make as many Cell comparison operations as required in order to find out the correct order.

If ever a comparison operation comes back as 0 -- i.e. the rows are the same -- then the SorterBase now trundles off through its SecondarySortOrder collection. It makes the appropriate IComparer for the next secondary sorting column -- NumericComparer, etc. -- and gets the appropriate Cells from both rows being compared. Then it sees what this IComparer makes of it, taking sort order into account. This is repeated until either it runs out of secondary sorting columns or a greater-than or less-than result is returned from the comparison. Each inheritor of ComparerBase now only performs Cell-based comparisons.

History

I decided to continue from where the original left off, but jump to Version 1.1. I've included a couple of bug fixes that were mentioned on the forum.

  • 17th June, 2007 - Initial release. (1.1.0)
    • Bug Fix: CellCheckBoxEventArgs (Paul Sprague)
    • Bug Fix: Cursor bug? (Peter Stuer)
  • 2nd July, 2007 - Bug fix release. (1.1.1)
    • Bug Fix: Removed hardcoded Top alignment
    • Bug Fix: Fixed erratic painting of selection (jover)
    • Bug Fix: Fixed error if no rows added to table (jover)

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