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