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.
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;
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"));
Cell cell = new Cell("columns 2 and 3");
cell.ColSpan = 2;
row.Cells.Add(cell)
row.Cells.Add(new Cell("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.
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;
table.EnableWordWrap = true;
Row row1 = new Row();
Cell cell1 = new Cell("This is a cell with quite long text");
cell1.WordWrap = true;
row1.Cells.Add(cell1);
Row row2 = new Row();
Cell cell2 = new Cell("This is long text that will just be truncated");
cell2.WordWrap = false;
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.
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();
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);
Row subrow = new Row();
subrow.Cells.Add(new Cell());
Cell cell = new Cell(subject);
cell.ForeColor = Color.Gray;
cell.ColSpan = 2;
subrow.Cells.Add(cell);
row.SubRows.Add(subrow);
subrow = new Row();
subrow.Cells.Add(new Cell());
cell = new Cell(preview);
cell.ForeColor = Color.Blue;
cell.ColSpan = 2;
cell.WordWrap = true;
subrow.Cells.Add(cell);
row.SubRows.Add(subrow);
}
Implementation
When a sub-row is added to the SubRows
collection, it is in fact added to the main table's collection of Row
s 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 Row
s 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:
With multi-column sorting:
Using the code
Create a SortColumnCollection
and add SortColumn
s to it as required. Then set this collection as the ColumnModel.SecondarySortOrders
property.
SortColumnCollection sort = new SortColumnCollection();
sort.Add(new SortColumn(3, SortOrder.Ascending));
sort.Add(new SortColumn(2, SortOrder.Ascending));
sort.Add(new SortColumn(1, SortOrder.Descending));
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 Cell
s 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)