Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

TreeView with Columns

4.79/5 (87 votes)
20 Feb 2008CPOL9 min read 5   69.8K  
TreeView with Columns and (partially) Design Time support
test_tree_validation.png

test_tree_folderview.png

Image 3

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.

C#
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.

C#
protected virtual object GetData(Node node, TreeListColumn column)  
{  
if (node[column.Index] != null)  
return node[column.Index];  
return null;  
}  

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:

C#
[Editor(typeof(ColumnCollectionEditor),typeof(System.Drawing.Design.UITypeEditor))]

The only customization I had to add to the editor was the following:

  • 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.
TreeListCollectionEditor.png

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

All mouse handling is done by overwriting base class mouse handling methods.

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:

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

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

C#
[Category("Columns")] 

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

C#
[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:

C#
[DesignerSerializationVisibility(DesignerSerializationVisibility.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:

C#
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] 

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

C#
[DefaultValue(20)]  
[DefaultValue(typeof(Padding), "0,0,0,0")]  
[DefaultValue(typeof(Color), "ControlText")] 

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...

C#
[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.

C#
[Editor(typeof(ColumnCollectionEditor),typeof(System.Drawing.Design.UITypeEditor))] 

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:

C#
[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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)