Introduction
Many people are looking at Visual Studio IDE as a flagship of application GUIs. Everybody likes its neat-looking interface, which is all so dockable, sliding and (yum!) tabbed. Everybody is eager to reproduce this beauty in their applications. But, (surprise!) the standard .NET controls do not provide half of the features displayed by Visual Studio itself. The good news is that .NET controls provide many more hooks and handles for enhancement and adaptation, compared to their ActiveX predecessors. The main feature that ActiveX does not have and the .NET controls have is inheritance. The control's object model is also cleverly designed and gives many possibilities for adaptation.
In this article, we shall discuss how to enhance TreeView
control's LabelEdit
ability to make it look more like the tree control used in Solution Explorer of VS environment. This article is also meant to start a series of the articles, explaining how to expand TreeView
abilities and supply it with some of the much desired functionalities.
The Problem
The TreeView
control that comes with .NET library is a very convenient way of displaying hierarchical data structures and objects. Its functionality is however significantly limited, compared to other similar controls used in Visual Studio IDE, Windows XP Explorer and other top-notch GUI applications. The main features expected from an up-to-date tree control are:
- multiselection (the ability to select multiple nodes simultaneously)
- richer drag-and-drop support, including drop position highlighting, drop validation, full-colored drag imaging
- node label edit customization and validation (which this article is about)
- pluggable external node label editors (like
ComboBox
, Calendar
, SpinBox
, etc.) - node image overlay (the ability to display small attributive images over regular node image, like exclamation signs, asterisks and others)
- built-in support for node cut/copy/paste and undo/redo stack, but this is arguable, because these features are very task-specific
Fill on expand behavior, which is also frequently mentioned as a desirable tree feature, is easily reproduced in the standard TreeView
with very little amount of code, so I don't really think we should discuss it here.
For example, let us look at the VS tree control (used in Solution Explorer and Class View). The first thing that meets the eye is customized label editing, implemented for root node in Solution Explorer. In the node text, we can see something like:
Solution 'Solution1' (1 project)
It is however changed to Solution1
when we click it to start label editing. If we change it to Solution2
and finish editing, we shall see that the label turns to:
Solution 'Solution2' (1 project)
We see that the label text is preprocessed before editing and post-processed after. That kind of behavior is called LabelEdit
customization and it can be of much use in many cases. For example, if we want to permit the user to edit only the name of a file and leave the extension intact. Or in case we want our nodes to be more self-describing, like the solution node in VS, e.g., method 'GetItems
' or property 'IsCreated
', but expose for editing only the naming part of it.
At first glance, it seems that we can easily imitate this kind of behavior using the TreeView
events BeforeLabelEdit
and AfterLabelEdit
. That seems natural and that the events with names like these could only be intended for preprocessing and postprocessing of label text, and also maybe for edit validation (which we will speak about later in this article). But nope! These events do not permit either of these functions. You will soon find out that there is no way to change LabelEdit
box content from BeforeLabelEdit
event because this event occurs after the LabelEdit
editing box is displayed. That is really strange, because it makes this event practically meaningless. It is hard to invent any possible application this event could be used for. Worse than this, you will find out that the AfterLabelEdit
event is also completely useless because it does not allow you to do any postprocessing of label edited text. Oh, you can see this text in e.Label
all right, although it is read-only, and you can have e.Node.Text
changed as you wish, like:
private void treeView1_AfterLabelEdit(object sender,
System.Windows.Forms.NodeLabelEditEventArgs e) {
e.Node.Text = e.Label + "hahaha";
}
But all this is in vain, because the changed e.Node.Text
lives only up to the end of the event handler. It will be changed shortly with the e.Label
value without any further event or notice. Yes, it is shocking, beyond logic, completely incomprehensible, but this is true. The BeforeLabelEdit
and AfterLabelEdit
events are completely useless and are made only to tease the honest .NET programmers. :)
But never give up. Another possible way of customizing node text editing that quickly comes to mind is intercepting the user intention to start label editing through other events - MouseDown
, KeyDown
, or Menu click (MouseUp
and Click
won't do because they occur too late to change anything). After understanding that the user really goes to start label editing, we can change TreeView.SelectedNode.Text
so that LabelEdit
box gets custom-formatted string
for editing. That approach seems to be OK, but after writing a test program, you will quickly discover that node text content is visibly changed about half a second before the edit starts resulting in visually uncomfortable behavior. And, of course, we see no hint of such behavior in Visual Studio Solution Explorer tree.
So, after studying different approaches and doing in-depth research of TreeView
class' overridable members, we come to a sad conclusion that there is no easy way to achieve what we would like. Of course, there is always a possibility to suppress built-in label editing and write your own on top of TreeView
control. This option seems quite feasible, albeit complicated. But it happens that there is still a much simpler way.
Solution
There is a very useful method in the .NET base Control
class called OnNotifyMessage
which can be used as a loophole to enhance .NET controls. If a control is configured by putting the following line in its constructor:
this.SetStyle(ControlStyles.EnableNotifyMessage, true);
then we can intercept WM_Messages
by overriding this method in the inherited class. Writing a simple ListBox
-based monitor to study sequences of wm_messages
in different situations can be very helpful. If we make a test program to monitor events and wm_message
sequences, that occur in the course of TreeView
label editing, we quickly notice that there is a certain WM_TIMER
event, which occurs immediately before LabelEdit
box appears. And that very moment is the most suitable time to substitute the node text, customizing it for editing. Most probably, this message is used to distinguish double click from single click, after all, double click should not start label editing, should it? And that must be the main cause of the pesky delay, that occurs between user click and the actual start of label editing, which caused us so much trouble earlier.
So the idea is to detect user intention to start label editing by intercepting WM_TIMER
message in the overridden OnNotifyMessage
method, substitute node text with the customized version (e.g. Solution1 instead of Solution 'Solution1' or filename instead of filename.ext), intercept the AfterLabelEdit
event in the overridden OnAfterLabelEdit
method, and transform the edited label back to the original format (e.g. newfilename to newfilename.ext).
private const int WM_TIMER = 0x0113;
private bool TriggerLabelEdit = false;
private string viewedLabel;
private string editedLabel;
protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e) {
this.SelectedNode.Text = viewedLabel;
}
protected override void OnAfterLabelEdit(NodeLabelEditEventArgs e) {
this.LabelEdit = false;
e.CancelEdit = true;
if(e.Label==null) return;
ValidateLabelEditEventArgs ea =
new ValidateLabelEditEventArgs(e.Label);
OnValidateLabelEdit(ea);
if(ea.Cancel==true) {
e.Node.Text = editedLabel;
this.LabelEdit = true;
e.Node.BeginEdit();
}
else
base.OnAfterLabelEdit(e);
}
public void BeginEdit() {
StartLabelEdit();
}
protected override void OnNotifyMessage(Message m) {
if(TriggerLabelEdit)
if(m.Msg==WM_TIMER) {
TriggerLabelEdit = false;
StartLabelEdit();
}
base.OnNotifyMessage(m);
}
public void StartLabelEdit() {
TreeNode tn = this.SelectedNode;
viewedLabel = tn.Text;
NodeLabelEditEventArgs e =
new NodeLabelEditEventArgs(tn);
base.OnBeforeLabelEdit(e);
editedLabel = tn.Text;
this.LabelEdit = true;
tn.BeginEdit();
}
The following code should be added to the OnMouseDown
, OnMouseUp
, OnClick
and OnDoubleClick
methods to cover up all the possible LabelEdit
situations:
protected override void OnMouseDown(MouseEventArgs e) {
if(e.Button==MouseButtons.Right) {
TreeNode tn = this.GetNodeAt(e.X, e.Y);
if(tn!=null)
this.SelectedNode = tn;
}
base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e) {
TreeNode tn;
if(e.Button==MouseButtons.Left) {
tn = this.SelectedNode;
if(tn==this.GetNodeAt(e.X, e.Y)) {
if(wasDoubleClick)
wasDoubleClick = false;
else {
TriggerLabelEdit = true;
}
}
}
base.OnMouseUp(e);
}
protected override void OnClick(EventArgs e) {
TriggerLabelEdit = false;
base.OnClick(e);
}
private bool wasDoubleClick = false;
protected override void OnDoubleClick(EventArgs e) {
wasDoubleClick = true;
base.OnDoubleClick(e);
}
How to Use It
This code pretty much covers all the possible situations of label editing, and resolves them in a manner exactly similar to the Visual Studio Solution Explorer tree behavior. It also gives the revised BeforeLabelEdit
a new meaning, which is much closer to its true purpose. The event occurs immediately before label editing starts, so that you can actually use it to substitute tree node text with the new value customized for editing.
private void Tree1_BeforeLabelEdit(object sender,
NodeLabelEditEventArgs e) {
TreeNode tn = Tree1.SelectedNode;
switch(tn.ImageIndex) {
case 0:
tn.Text =
System.IO.Path.GetFileNameWithoutExtension(tn.Text);
break;
case 1:
tn.Text = GetQuotedName(tn.Text);
break;
}
}
private string GetQuotedName(string label) {
int pos1 = label.IndexOf("\"") + 1;
int pos2 = label.LastIndexOf("\"");
if((pos2-pos1)>0)
return label.Substring(pos1, pos2 - pos1);
else
return "";
}
private void Tree1_AfterLabelEdit(object sender,
System.Windows.Forms.NodeLabelEditEventArgs e) {
TreeNode tn = Tree1.SelectedNode;
switch(tn.ImageIndex) {
case 0:
tn.Text = e.Label +
System.IO.Path.GetExtension(tn.Text);
break;
case 1:
break;
}
}
Input Validation
Another thing that is nice to have in a TreeView
control is LabelEdit
input data validation. As you can see in Visual Studio IDE, an error message pops up in case your input in label editing was invalid, like 'You must enter a name', if you had tried to input an empty string
as a solution name. After clicking OK on the error message label, edit continues from the initial value (i.e., the value it had when the editing started). To make this functionality available for users of our enhanced TreeView
control, we have provided an additional event for our control.
ValidateLabelEdit(object sender, ValidateLabelEditEventArgs e)
The event is using the new ValidateLabelEditEventArgs
class, made by inheriting from CancelEventArgs
and adding a Label
property.
Here is an example of how the ValidateLabelEdit
event handler can be implemented:
private void Tree1_ValidateLabelEdit(object sender,
ValidateLabelEditEventArgs e) {
if(e.Label.Trim()=="") {
MessageBox.Show("The tree node label cannot be empty",
"Label Edit Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
e.Cancel = true;
return;
}
if (e.Label.IndexOfAny(new char[]{'\\',
'/', ':', '*', '?', '"', '<', '>', '|'})!=-1) {
MessageBox.Show("Invalid tree node label.\n" +
"The tree node label must not contain " +
"following characters:\n \\ / : * ? \" < > |",
"Label Edit Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
e.Cancel = true;
return;
}
}
Summary
What we have found is that BeforeLabelEdit
and AfterLabelEdit
events of TreeView
give you no control over the LabelEdit
process, except the possibility to cancel it out.
On the other hand, there is a need to control this process, for example:
- when you want to edit only a part of the
TreeNode
name and leave the rest intact (as in VS Solution Explorer), - when you want to validate user input and prevent
LabelEdit
to finish with unwanted result text (as in Windows Explorer or VS Solution Explorer).
After careful research, we have found that the most easy and unobtrusive way to achieve this is the following:
- Inherit from
TreeView
. - Override the
OnNotifyMessage
method to intercept wm_messages
. - Trap the
WM_TIMER
message in the context of LabelEdit
operation. - Override
OnBeforeLabelEdit
and OnAfterLabelEdit
, and also OnMouseDown
, OnMouseUp
, OnClick
and OnDoubleClick
to give LabelEdit
process a consistent and logical behavior. - Add a new event
ValidateLabelEdit
.
As a result, we have obtained an enhanced TreeView
control, which gives the possibility to use familiar BeforeLabelEdit
and AfterLabelEdit
events to customize LabelEdit
and the new ValidateLabelEdit
event to check edit result and prevent LabelEdit
completion in case of unwanted input.
History
- October 12th, 2005 - Article submitted
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.