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
TreeViewdoes 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.
NodesCollectionwas for the most part straightforward. The
node contains a
Nextpointer (linked list) and the
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
HasChildrenwas added. Now when a node is added to the tree, if
HasChildrenis 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
FolderViewtree 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
VisibleNodeCountjust returns the total visible count.
To verify that the count was correct, I added both
slowTotalRowCountand 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
AutoSizeoption. 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
AutoSizecolumns 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
CollectionEditordepends on the
IList interface, and sure enough
IList<> and the
IList interface. And once I added the
IList interfaceto 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
CollectionEditor, and then I assigned this editor to the
collectionclass 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
IsInputKeyis 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
painterclasses 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
FolderViewis an example where I override
GetNodeBitmapto get the image associated with the current file type. One interesting note regarding getting the icon for a file. The
icon class has an
ExtractAssociatedIconmethod, 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
trueto 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
Initializemethod, 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
ViewSettingclass exposed as
ViewOptionsproperty in the tree view:
To avoid all properties from being serialized in
Initializethe 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
stringto an object or vice versa, or it can be used to simply provide a name for the given object, for instance
OptionsSettingTypeConverterprovides names for the different
To assign a type converter to a class, add the attribute...
OptionsSettingTypeConvertmust 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
CollectionEditorand 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
ControlDesignerderived class and attach it to the custom control.
TreeListViewDesigneris attached to the
TreeListViewwith 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.