Click here to Skip to main content
Click here to Skip to main content

XPTable: .NET ListView Update

By , 4 Jul 2007
 

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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

adambl
United Kingdom United Kingdom
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionWordWrapping problemmemberMember 850712912 Mar '13 - 22:38 
Hi,
My XPTable in my C# windows application has a word wrapping issue.
I don't want it to wrap words but that's what it is doing.
I tried to solve by setting the EnableWordWrapping to false but it's still wrapping.
It was working fine 2 years ago but all of a sudden it has started wrapping word.
I really need help. please
QuestionXPTable: Word Wrapping IssuememberMember 850712912 Mar '13 - 22:33 
Hi,
My XPTable in my C# windows application has a word wrapping issue.
I don't want it to wrap words but that's what it is doing.
I tried to solve by setting the EnableWordWrapping to false but it's still wrapping.
It was working fine 2 years ago but all of a sudden it has started wrapping word.
I really need help. please
AnswerRe: XPTable: Word Wrapping Issuememberadambl13 Mar '13 - 6:00 
When you say it "all of a sudden has started wrapping" do you mean that you have used a newer version of XPTable? Or is it exactly the same code and XPTable DLL? What else has changed over the last 2 years?
 
Adam
GeneralRe: XPTable: Word Wrapping IssuememberMember 850712919 Mar '13 - 22:33 
The only thing that has changed is the fact that I moved the project from VS 2008 to VS 2010.
The project and the XPTable DLL are the same.
I don't have a newer version of XPTable.
Bugrowcollection bugmemberPaul Hildebrandt9 Jan '13 - 9:22 
I came across a null exception when using XPTable. It happened when I tried to update a DateTime field in the dataset linked to the table, the error happens when I called dataset.AcceptChanges().
 
I traced the error to XPTable.Models.RowCollection.Clear(). The line:
this[0].InternalTableModel.Table.ClearAllRowControls();
was throwing the exception. It turns out the InternalTableModel was null.
I just added this null check and everything seemed to work fine:
if (this[0].InternalTableModel != null)
   this[0].InternalTableModel.Table.ClearAllRowControls();
I'm not very proficient at programming components so let me know if this is incorrect.
GeneralRe: rowcollection bugmemberadambl28 Jan '13 - 11:31 
Hi, I can't reproduce the error you are seeing (I am using the latest code from SF), but the change you made is fine.
 
Adam
Questiondatabinding overrides settings?memberPaul Hildebrandt5 Jan '13 - 10:24 
I bound a XPTable to a datatable (fairly straight forward). However whenever I change the settings (Eg. column width, editable) in the column model of the table they get reset to the default values. I can set them in the forms constructor.
 
Also AutoResizeColumnWidths doesn't seem to do anything. I even tried to reset() the table first.
AnswerRe: databinding overrides settings?memberadambl28 Jan '13 - 11:48 
That not right, I can get it to do what you want (I am using the latest code from SF).
 
If you download the demo solution from SF here[^], first of all get the 'DataBinding' project running (just press F5!). Now, in the Demo form in that project, replace the button1_Click() code with this:
 
bool first = true;
 private void button1_Click(object sender, System.EventArgs e)
 {
     if (first)
     {
         this.table.ColumnModel.Columns[1].Width = 200;
         this.table.ColumnModel.Columns[2].Editable = false;
     }
     else
     {
         this.table.ColumnModel.Columns[2].Editable = true;
         this.table.AutoResizeColumnWidths();
     }
     first = !first;
 }
 
Run it again, and this time, when you click the 'Go' button at the bottom, the first time you do it, the column width of the 'size' column should change and the 'date' column should no longer be editable. If you click the button again, the columns should auto-resize and the 'date' column will once again be editable.
 
Let me know how you get on...
 
Adam
BugIssue in showing scrollbarsmemberCptHook17 Oct '12 - 3:45 
Hi,
Vertical Scrollbar is not shown correctly using subrows with EnableWordWrap=true and Scrollable=true properties. It seems that the height value of the items used to calculate if they can fit in the client area doesn't take in account the height after the text has been wrapped.
 
Has someone found a solution for this?
 
Thanks in advance!
GeneralRe: Issue in showing scrollbarsmemberadambl19 Oct '12 - 12:43 
Hi,
 
Are you using the source from this article (out of date I'm afraid) or from SF (and if so, latest code from SVN or a release)?
 
Adam
GeneralRe: Issue in showing scrollbarsmemberCptHook30 Oct '12 - 23:23 
Hi,
I'm using the release 1.2.2 I downloaded from SF...
 
Thanks!
GeneralRe: Issue in showing scrollbarsmemberadambl28 Jan '13 - 11:21 
Did you get anywhere with this?
 
I have just tried to see what you mean, and did see one thing that you may see as a problem, but I'm not sure exactly what you mean by 'not shown correctly'.
 
What I saw was that when the table initially loads, the scrollbar is shown, but calculates the height of the thumb (the 'block' you drag up and down) with the assumption that hidden rows have the default row height. As the scroll down, the scrollbar gets better and better at calculating the size of the thumb.
 
This is because XPTable only renders rows that need rendering (an important part of keeping it performing well for high row counts), and so is unavoidable.
 
However, as I say, I am not sure this is what you meant by 'not correct'.
 
Do you have more info on a fix, or more info on the nature of the problem?
 
Adam
GeneralRe: Issue in showing scrollbarsmemberCptHook28 Jan '13 - 23:40 
Hi Adam,
What I mean is that it seems that the vertical scrolling bar is shown only when the total rows height is major than the listview control height, BUT the row height is calculated without taking in account the row height after the text in cells has been wrapped.
As you can see in the following image http://img824.imageshack.us/img824/8611/histchanges.png[^] the counter on the form title count 6 elements, but I cannot see all the items because the scrolling bar has not been shown.
 
Hope to be clear.
 

Thank you very much.
 
Stefano
GeneralRe: Issue in showing scrollbarsmemberadambl29 Jan '13 - 12:10 
Ah yes, I see exactly what you mean now.
 
There is a quick way to sort this out:
Add a handler to the Table.AfterFirstPaint event:
void table_AfterFirstPaint(object sender, EventArgs e)
{
    table.UpdateScrollBars();
}
 
The scrollbar calculation can only be done properly after the rows have been Painted, as this is the first time their height has been properly determined.
 
I will make a change to XPTable so that this happens automatically, but for now the quick solution should sort you out.
 
Adam
GeneralRe: Issue in showing scrollbarsmemberadambl29 Jan '13 - 22:07 
I have added this change to the SF code, SVN rev 210:
 
If word wrapping is enabled, then UpdateScrollBars() is called just after the first paint event, so that scrollbars are correctly calculated, taking into account the actual rendered height of rows. Without this you sometimes did not see the scrollbar when XPTable loaded, as it had assumed the default row height for all rows when deciding to show the scrollbar of not.
 
Adam
GeneralRe: Issue in showing scrollbarsmemberCptHook5 Feb '13 - 5:56 
Hi Adam,
Thank you very much for your work!
 

Best,
Stefano
GeneralRe: Issue in showing scrollbarsmemberCptHook13 Feb '13 - 1:20 
Hi Adam,
I updated the project from SVN (latest rev 211), but it seemes changes you made to solve this issue have been lost...
 
Frown | :(
 
Can you please check them? Cool | :cool:
 

Thank you very much!
 
Stefano
GeneralRe: Issue in showing scrollbarsmemberadambl13 Feb '13 - 4:11 
This shows the diff for rev 210 : SF Svn browse[^].
 
It adds the new method FirstPaint():
 
7625 private void FirstPaint()
7626 {
7627     OnAfterFirstPaint(EventArgs.Empty);
7628 
7629     // Do this so that scrollbars are evaluated whilst the actual row heights are known
7630     if (this.EnableWordWrap)
7631     {
7632         if (autoCalculateRowHeights)
7633             this.CalculateAllRowHeights();
7634         this.UpdateScrollBars();   // without this the scolling will have been set up assuming all rows have the default height
7635     }
7636 }
 
which is called by the method above it, OnPaint().
 
Can you see those?
 
Adam
QuestionHey Adam, varying cell renderer per row, possible?memberNWGaEagle27 Jun '12 - 12:25 
Is it possible to have a combobox for each row but have different selections in each one (so, "a", "b", "c" in row 1, but "d", "e", "f" in row 2, etc). It looks like that is all controlled in the ColumnModel as opposed to the TableModel.
 
Thanks for any help you can offer.
 
You can tweet me at nw ga eagle (no spaces) to ask any detailed questions.
AnswerRe: Hey Adam, varying cell renderer per row, possible?memberadambl27 Jun '12 - 23:25 
You can do this if you attach a handler to the Table.BeginEditing event.
 
This is fired when the user starts to edit any cell, but before the editor control (whatever that is) is shown to the user. You can check the column is the one you want, then use the row index to set the appropriate values for the combo:
 
void table_BeginEditing(object sender, XPTable.Events.CellEditEventArgs e)
{
    if (e.Column == 0)
    {
        ComboBoxCellEditor edit = e.Editor as ComboBoxCellEditor;
        if (edit != null)
        {
            edit.Items.Clear();
            edit.Items.Add("apple");
            edit.Items.Add("pear");
            edit.Items.Add("grapes");
            edit.Items.Add("row " + e.Row.ToString());
        }
    }
}
 
Regards, Adam
GeneralRe: Hey Adam, varying cell renderer per row, possible?memberNWGaEagle28 Jun '12 - 2:22 
Excellent, you're awesome!
QuestionAutoResizeColumnWidths() doesn't exist?memberMember 771704026 Jun '12 - 8:41 
I see a lot of references to using a function called Table.AutoResizeColumnWidth() to automatically resize the table so all columns are properly sized. I do not see this function in the source code I downloaded! It doesn't show up in Intellisense either. What is going on?
AnswerRe: AutoResizeColumnWidths() doesn't exist?memberadambl26 Jun '12 - 12:03 
The source code attached to this article is out of date now. Please download the latest code or DLL from Source Forge. See the message just below[^].
 
Please note that the method is actually
AutoResizeColumnWidths()
 
Regards, Adam
Questionword wraping problem for more that one cell in a rowmemberStart at 4021 Mar '12 - 1:19 
i found out that if we have more than one wrapable cells in a row, the row height will follow the most right wrapable cell.
 
eg: if cell.1 consume two line but cell.2 consume only one line and both is mark as wordwarp true, the row height will be one line.
 
is there a way two overcome this problem. thanks
AnswerRe: word wraping problem for more that one cell in a rowmemberAbris14 May '12 - 2:43 
You have probably solved it already, but I had the same problem and I changed the function "GetRenderRowHeight" as shown below (what is does is to loop through all the cells and return the maximum height of the cell in each row)
 
int GetRenderedRowHeight(Graphics g, Row row)
       {
           int height = row.Height;
 
           if (row.HasWordWrapCell)
           {
               foreach (Cell varCell in row.Cells)
               {
                   int column = varCell.InternalIndex;
                   if (varCell.WordWrap)
                   {
                       // get the renderer for the cells column
                       ICellRenderer renderer = this.ColumnModel.Columns[column].Renderer;
                       if (renderer == null)
                       {
                           // get the default renderer for the column
                           renderer = this.ColumnModel.GetCellRenderer(this.ColumnModel.Columns[column].GetDefaultRendererName());
                       }
 
                       // When calling renderer.GetCellHeight(), only the width of the bounds is used.
                       int w = this.GetColumnWidth(column, varCell);
                       renderer.Bounds = new Rectangle(this.GetColumnLeft(column), 0, this.GetColumnWidth(column, varCell), 0);
 
                       // If this comes back zero then we have to go with the default
                       int newheight = renderer.GetCellHeight(g, varCell);
                       //Console.WriteLine("    GetRenderedRowHeight colwidth={0} rowheight={1}", w, newheight);
                       if (newheight == 0)
                           newheight = row.Height;
                       height = Math.Max(height, newheight);
                   }
               }
           }
           return height;
       }

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 4 Jul 2007
Article Copyright 2007 by adambl
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid