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.
NodesCollection are implemented in TreeListNode.cs.
NodesCollection was for the most part straightforward. The
node contains a
Next pointer (linked list) and the
NodesCollection contains a
LastNode and a
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
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
slowTotalRowCount and I check the two values in a node validation when
Validate is clicked on the "Tree Validation" tree.
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.
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
protected virtual object GetData(Node node, TreeListColumn column)
if (node[column.Index] != 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
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
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
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.
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
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.
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
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
All mouse handling is done by overwriting base class mouse handling methods.
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
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.
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:
To hide a property from the property grid, set (or
true to show a property which is hidden in the base class):
Hiding a property grid does not necessarily prevent the property from being serialized in the
Initialize method, to prevent this set the
If the property is a class and the properties in the class should be serialized then set the
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
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
To assign a type converter to a class, add the attribute...
OptionsSettingTypeConvert must derive from either
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:
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.
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.