TreeView with Columns and (partially) Design Time Support

I have written several custom controls over the last couple of years, some of the controls were written from scratch, and others were enhancements to existing (3rd party) controls. What was common for all of the controls was that I didn't pay any attention to how to implement design time support.

Since the standard Microsoft .NET TreeView does not support columns, I decided it would be a useful and fun project to write a tree control that supports columns from scratch, and at the same time I could learn about the design time side of writing custom controls.

The tree supports:

Columns, fixed sized and auto sized. Can be added at design time. Header and cell format and color can be set at design time.

Reorder visible columns, hide / show columns (programmatically only)

Image list and image index for nodes

Single / Multi select

Easy to overwrite cell paint

Build child nodes on demand

What is not supported (yet):

Sorting of columns

Cell edit

Re-arranging columns at run time

Individual row height. Only fixed row height is supported

Node and NodesCollection

Before implementing the node and node collection, I considered whether to use a list List<Node> for node collection, or to keep the nodes as a linked list. In my experience I have almost never had to access nodes by index directly, but rather through iteration. However, I have often had to remove and insert nodes before or after other nodes, so for this reason I decided on the linked list implementation.

Node and NodesCollection are implemented in TreeListNode.cs.

CommonTools.Node
CommonTools.NodeCollection

Implementing the Node and NodesCollection was for the most part straightforward. The node contains a Prev/Next pointer (linked list) and the NodesCollection contains a FirstNode / LastNode and a count.

Performance Issues

A common approach when building large trees is to build the child nodes of a node only before the node is expanded for the first time. To support this and at the same time still show the node as having children, even when in fact it is empty, the property HasChildren was added. Now when a node is added to the tree, if HasChildren is set it will show the plus/minus sign and it is then the developers responsibility to fill in the child nodes on the callback event NotifyBeforeExpand or override OnNotifyBeforeExpand.

The FolderView tree is an example of building the child nodes on demand.

Another performance issue to consider when building a tree is how to get the total number of visible nodes. The GUI part of the tree needs to know how many rows are visible in order for it to adjust the vertical scrollbar. For instance, if there is one root node with 10 children and the node is collapsed the visible row count is 1, and when the node is expanded the visible row count now changes to 11. The slow approach to this is to iterate through all visible nodes, but clearly this is not ideal for a large tree.

The solution to this is to have each node notify its parent when the visible count changes, this way any change will propagate all the way up to the root collection and now access to VisibleNodeCount just returns the total visible count.

To verify that the count was correct, I added both VisibleNodeCount and slowTotalRowCount and I check the two values in a node validation when Validate is clicked on the "Tree Validation" tree.

Columns

There is not much to the TreeListColumn class. It contains formatting for the header and the cells, the caption and fieldname, the default size and the auto size mode.

The TreeListColumnCollection is a little more interesting. The collection contains a list of the columns in the order they have been added. This is used for accessing the data in the node by index and is the default implementation for the GUI's GetData.

It also contains a list of the visible columns which is what is used when painting the tree. Whenever a column is resized or the tree is resized, the visible column's rectangles are being recalculated. This is done in RecalcVisibleColumsRect.

The column has an AutoSize option. When this is enabled, the column cannot be resized. Instead the width of the column will be set to the minimum size set in AutoSizeMinSize plus a ratio of the remaining width. The ratio is found by adding up all the ratio values from the different AutoSize columns and then dividing it by the remaining width. An example of the auto size is AutoSize where the first column has a ratio of 100 and the second column has a ratio of 50, so the first column will get 2/3 of the remaining width while the second column will get 1/3.

Design Time Issues for ColumnsCollection

At first when I implemented the columns collection, it didn't show the ellipses button (…) in the property grid, and no matter what I tried I couldn't get it to show up. I found that if I derived from CollectionBase or List<TreeListColumn> then it would show. But if I implemented only IList<TreeListColumn> then it would not show.

The obvious solution would have been to derive from List<> and then override the APIS, but instead I decided to figure out why it didn't work with the IList<> interface.

After doing some investigating using Reflector, I found that the default CollectionEditor depends on the IList interface, and sure enough List<> implements both IList<> and the IList interface. And once I added the IList interface to the collection, it showed up in the property grid.

ColumnCollectionEditor

To give the column a unique caption and fieldname when created, I created a new editor ColumnCollectionEditor derived from CollectionEditor, and then I assigned this editor to the collection class with the Editor attribute:

CreateInstance, which is called when Add is clicked in the designer. Here a new column is created with a unique fieldname.

GetDisplayText, this is the text shown in the list, I chose to show caption(fieldname) and keep it read only, and finally

EditValue, this is called after a value has changed. Here I refresh the tree to reflect changes in the GUI immediately.

The ‘GUI’

The control itself is mostly straightforward. Obviously for a tree control you need scrollbars, so first I derived from ScrollableControl, but I ran into some issues with the vertical scrolling, so instead I ended up deriving from Control and adding the scrollbars myself.

Whenever the size of the control changes or the number of visible nodes changes, the scrollbars are updated with UpdateScrollBars().

When painting the nodes, the control needs to know the first ‘screen-visible’ node which is determined by the vertical scroll position. To avoid iterating through the visible nodes each time paint is called, the control keeps track of the visible node with m_firstVisibleNode, and this node is updated when the vertical scroll bar is scrolled in OnVScroll.

Mouse Handling

Keyboard Handling

To enable key events to be forwarded to your Control derived control it is necessary to overwrite. By default key events are not forwarded to a control derived from Control, instead IsInputKey is returned for each key which is to be handled by the control. In my case I handle the arrow keys, page up/down and home/end.

Painting the Tree

I have tried to keep painting the tree flexible and easy to override by providing virtual methods and painter classes for the different elements of the tree. For instance, drawing the column headers are done by calling Columns.Draw() which calls the CollumnCollections Painter.DrawHeader. For the nodes there are a couple of virtual methods which can be overwritten, all of which eventually call into CellPainter.

The FolderView is an example where I override GetNodeBitmap to get the image associated with the current file type. One interesting note regarding getting the icon for a file. The icon class has an ExtractAssociatedIcon method, unfortunately this method does not return any icons for folders, and did not return the correct icon for all file types. After some online searching I found the solution in a tutorial here using the shell called SHGetFileInfo. The code for this is in IconUtil in Util.cs, including a full link to the tutorial I found.

Design Type Attributes and Converters

Since the design time support is still new to me, I can't give any detailed explanation of how it works, instead I will summarize what I have learned.

Attributes

The description shown in the property grid is set with:

[Description("This is the columns collection")]

The category where the property is to show in the property grid is set with:

[Category("Columns")]

To hide a property from the property grid, set (or true to show a property which is hidden in the base class):

[Browsable(false)]

Hiding a property grid does not necessarily prevent the property from being serialized in the Initialize method, to prevent this set the Visibility to Hidden:

If the property is a class and the properties in the class should be serialized then set the Visibility to Content, an example is the ViewSetting class exposed as ViewOptions property in the tree view:

To avoid all properties from being serialized in Initialize the default value attribute can be used. The property will then only be set if it differs from the default value. Default can be used for simple types or types which implement a type converter (can be initialized from a string):

A type convert is used to convert from a string to an object or vice versa, or it can be used to simply provide a name for the given object, for instance OptionsSettingTypeConverter provides names for the different Setting.

To assign a type converter to a class, add the attribute...

[TypeConverterAttribute(typeof(OptionsSettingTypeConverter))]

... where OptionsSettingTypeConvert must derive from either ExpandableObjectConverter or TypeConverter.

For a collection to show the collection editor, the class must implement IList interface. If any custom handling is required in the collection editor, then create an editor derived from CollectionEditor and attach the editor to the collection class with the attribute.

And finally, it is possible to forward mouse events to the custom control at design time by implementing a ControlDesigner derived class and attach it to the custom control. TreeListViewDesigner is attached to the TreeListView with the attribute:

[Designer(typeof(TreeListViewDesigner))]

This allows mouse events to be forwarded to the tree control at design time allowing the columns to be resized with the mouse.

I know there is much more to the design time support than what I have implemented, and that I have barely scratched the surface, but at least it has given me some basic knowledge of how to provide design time support for custom controls.

References

Book: Pro .NET 2.0 Windows Forms and Custom Controls in C#.

I purchased this book because of its two chapters on design time support, and this book was definitely a big help even though I did run into issues which it does not cover.

I'm not sure if that is supported be the default Microsoft tree view, but I know it is not supported by this tree. If you want to bind to a data source you must write a wrapper class yourself which builds the tree by iterating through your table (or set).
I explained in another post how to build the tree, but here it is again, I hope this helps.

A couple of points that might no be obvious. For any nodes to show a column must be added.
After the nodes are added EndUpdate (or RecalcLayout() ) must be called in order for the scroll bars to be recalculated.

It worked!
Thank you for your help.
Do you know whether I could enable VisualStyle on the remote machine, so the look and feel doesn't change?
I mean in the source code's main routine, the VisualStyle ist enabled. Is there any other switch I could turn the VisualStyle on?

I just downloaded your project and tested it on my local machine (WinXP Pro).
Everything works great so far. When I copy your project to a remote machine (Windows Server 2003 small business), the headings (e.g. your screenshot: Name, Absolute Index, etc.) aren't displayed.
Now my problem is that I don't know whether there is some trouble with the Server OS, OR with me seeing the remote server via Remote Desktop Connection.
I hope somebody can help me with this.

I think the problem is probably in ColumnHeaderPainter.DrawHeader, there if VisualStyles are not enabled then it draws a button using ControlPaint.DrawButton, but oops, I forgot to draw the text.
Try add DrawString, and maybe add the formatting (you have to convert TextFormatting to StringFormat.

I'm not sure if I can help, but I did see some design time issue too while working with this. I noticed if you include the CommonTools as part of your project and then change anything inthre, then Visual Studio must be restarted else it will give you some cast exception when trying to save changes from the designer. If instead you only include the dll then Visual Studio seems to be happy, but I'm not sure exactly why.

If you do find a solution to this please let me know as I mentioned in article, the design time support is all new to me