Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / Windows Forms

Enhancing TreeView: Customizing LabelEdit

Rate me:
Please Sign up or sign in to vote.
4.41/5 (21 votes)
19 Oct 20058 min read 146.4K   3.5K   58   27
The article describes how to supplement TreeView control's LabelEdit ability with some VS Solution Explorer like features, including label edit pre/post processing and input validation.

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:

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

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

C#
private const int WM_TIMER = 0x0113;
private bool TriggerLabelEdit = false;
private string viewedLabel;
private string editedLabel;

protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e) {
  // put node label to initial state
  // to ensure that in case of label editing cancelled
  // the initial state of label is preserved
  this.SelectedNode.Text = viewedLabel;
  // base.OnBeforeLabelEdit is not called here
  // it is called only from StartLabelEdit
}

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:

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

C#
private void Tree1_BeforeLabelEdit(object sender, 
                             NodeLabelEditEventArgs e) {
  // --- Here we can customize label for editing ---
  TreeNode tn = Tree1.SelectedNode;
  switch(tn.ImageIndex) {
    case 0:
      // strip filename from extension for editing
      tn.Text = 
        System.IO.Path.GetFileNameWithoutExtension(tn.Text);
      break;
    case 1:
      // extract quoted item name for editing
      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) {
  // --- Here we can transform edited label 
  // --- back to its original format ---
  TreeNode tn = Tree1.SelectedNode;
  switch(tn.ImageIndex) {
    case 0:
      // paste extension back to edited filename
      tn.Text = e.Label + 
                System.IO.Path.GetExtension(tn.Text);
      break;
    case 1:
      // restore full label 
      // formatText = "Item \"" + e.Label + "\"";
      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.

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

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

  1. Inherit from TreeView.
  2. Override the OnNotifyMessage method to intercept wm_messages.
  3. Trap the WM_TIMER message in the context of LabelEdit operation.
  4. Override OnBeforeLabelEdit and OnAfterLabelEdit, and also OnMouseDown, OnMouseUp, OnClick and OnDoubleClick to give LabelEdit process a consistent and logical behavior.
  5. 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.


Written By
Web Developer
Russian Federation Russian Federation
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugCauses NullpointerException if ValidateLabelEdit isn't initialized Pin
Member 127134101-Sep-16 2:45
Member 127134101-Sep-16 2:45 
QuestionLicense? Pin
PhilDHorne2-Dec-14 2:00
PhilDHorne2-Dec-14 2:00 
SuggestionYet another alternative Pin
Mathias Gorm Jensen10-Jul-14 2:13
Mathias Gorm Jensen10-Jul-14 2:13 
GeneralRe: Yet another alternative Pin
Zaksiz18-Dec-14 21:35
Zaksiz18-Dec-14 21:35 
GeneralA couple of minor changes Pin
Gil Kirkpatrick24-May-10 1:05
Gil Kirkpatrick24-May-10 1:05 
GeneralValidateLabelEdit() Pin
sinwatt6-Oct-09 14:35
sinwatt6-Oct-09 14:35 
GeneralMore AfterLabelEdit tricks Pin
mb1830-Jul-09 9:56
mb1830-Jul-09 9:56 
GeneralObservation for Visual Studio 2005 Pin
dusikov9-Feb-09 20:33
dusikov9-Feb-09 20:33 
GeneralThe lazy programmer variant Pin
Adrian Pirvu1-Feb-09 21:48
Adrian Pirvu1-Feb-09 21:48 
GeneralValidateLabelEdit Pin
TWang22-Feb-08 10:54
TWang22-Feb-08 10:54 
GeneralAn easier way to achieve this using only .NET without any API calls... Pin
tombala21-Mar-07 7:07
tombala21-Mar-07 7:07 
GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
miklovan21-Mar-07 22:23
miklovan21-Mar-07 22:23 
GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
tombala23-Mar-07 5:05
tombala23-Mar-07 5:05 
GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
miklovan23-Mar-07 5:26
miklovan23-Mar-07 5:26 
QuestionRe: An easier way to achieve this using only .NET without any API calls... Pin
stixoffire8-Jul-07 11:50
stixoffire8-Jul-07 11:50 
While inheritance is no big deal for me the "Fun win Api call" I can do with out...

I like this way because the control is portable - the whole idea is that I do not need the WIN API call - if I am running else where for example..

How would I implement this method in an iherited treeview so I do not need to write the code every time ? I want to be able to validate the nodes against regular expressions...

GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
SnapConfig6-Mar-08 15:15
SnapConfig6-Mar-08 15:15 
GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
SnapConfig7-Mar-08 15:06
SnapConfig7-Mar-08 15:06 
GeneralRe: An easier way to achieve this using only .NET without any API calls... Pin
MindlessCoder21-Aug-09 5:00
MindlessCoder21-Aug-09 5:00 
GeneralOr if it's just the validation you want ... Pin
Mike Hell5-Dec-06 23:15
Mike Hell5-Dec-06 23:15 
GeneraltreeView LabelEdit Pin
Aleksander_V_Kuznetsov29-Jun-06 0:32
Aleksander_V_Kuznetsov29-Jun-06 0:32 
QuestionVisual Basic .NET version? Pin
ggoodall22-Jun-06 8:43
ggoodall22-Jun-06 8:43 
GeneralWinApi way Pin
Florin Catana28-Mar-06 3:33
Florin Catana28-Mar-06 3:33 
GeneralRe: WinApi way Pin
Steven Roebert10-Aug-06 13:00
Steven Roebert10-Aug-06 13:00 
GeneralRe: WinApi way Pin
vanmelle18-Dec-07 14:03
vanmelle18-Dec-07 14:03 
GeneralAwesome! Pin
rubensr8-Mar-06 15:56
rubensr8-Mar-06 15:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.