Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Enhancing TreeView: Customizing LabelEdit

4.41/5 (21 votes)
19 Oct 20058 min read 1   3.5K  
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.