Recently I was updating a support tool that displays documents in raw form and allows editing of them. This tool is centred around a TreeView, and the Text property of each TreeNode is a concatenation of a name and one or more values.

The problem with this approach is if you wish to allow the nodes to be edited using the built in functionality you're in trouble as by default you can't actually influence the text that appears in the in-line editor. In other applications of a similar nature I used owner-drawn trees as I was using different styles for the name and the value. In this case, I just wanted the standard look.

An example project showing the different techniques described in this article
An example project showing the different techniques described in this article

How you would expect it to work

Ideally, you'd expect that by hooking into the BeforeLabelEdit event (or overriding OnBeforeLabelEdit) then you could manipulate the NodeLabelEditEventArgs.Label property. Except this property is read only.

Scratch that then. What about setting the TreeNode.Text property to something else in this event, then resetting it afterwards? Nope, doesn't work either.

Therefore, using just the managed code of the TreeView it's not possible to do what we want. Lets get slightly outside the black box with a little Win32 API. We'll get the handle of the edit control the TreeView is using and directly set it's text.

Getting the handle to the EDIT control

In order to manipulate the edit control, we first need to get a handle to it. We can do this succinctly by overriding OnBeforeLabelEdit (or hooking the BeforeLabelEdit event although the former is preferable) and using the TVM_GETEDITCONTROL message.

csharp
[DllImport("USER32", EntryPoint = "SendMessage", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

public const int TVM_GETEDITCONTROL = 0x110F;

protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e)
{
  IntPtr editHandle;

  editHandle = SendMessage(this.Handle, TVM_GETEDITCONTROL, IntPtr.Zero, IntPtr.Zero);
  if (editHandle != IntPtr.Zero)
  {
    // we have a handle, lets use it!
  }

  base.OnBeforeLabelEdit(e);
}

Setting the text of the EDIT control

Now that we have a handle, we can painlessly use WM_SETTEXT to change the text of the edit control

csharp
[DllImport("USER32", EntryPoint = "SendMessage", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, string lParam);

public const int WM_SETTEXT = 0xC;

protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e)
{
  // -- snip --

  if (editHandle != IntPtr.Zero)
    SendMessage(editHandle, WM_SETTEXT, IntPtr.Zero, "Magic String");

  // -- snip --
}

OK, but what about specifying the real text?

If you were hooking into the BeforeLabelEdit event, then you can just have your own logic in that event to determine the text to apply. If however you're overriding OnBeforeEdit in order to make a nice reusable component, you need another way of allowing implementers to specify the value. For this, I added a new event to the control.

csharp
[Category("Behavior")]
public event EventHandler<NodeRequestTextEventArgs> RequestEditText;

protected virtual void OnRequestEditText(NodeRequestTextEventArgs e)
{
  EventHandler<NodeRequestTextEventArgs> handler;

  handler = this.RequestEditText;

  if (handler != null)
    handler(this, e);
}

The NodeRequestTextEventArgs class is essentially a clone of NodeLabelEditEventArgs except with a writeable Label property. I also decided to allow you to cancel the node edit from this event, so that implementers don't have to hook both events unless necessary.

csharp
public class NodeRequestTextEventArgs : CancelEventArgs
{
  public NodeRequestTextEventArgs(TreeNode node, string label)
    : this()
  {
    this.Node = node;
    this.Label = label;
  }

  protected NodeRequestTextEventArgs()
  { }

  public TreeNode Node { get; protected set; }

  public string Label { get; set; }
}

Our final version now looks like this:

csharp
protected override void OnBeforeLabelEdit(NodeLabelEditEventArgs e)
{
  NodeRequestEditTextEventArgs editTextArgs;

  // get the text to apply to the label
  editTextArgs = new NodeRequestTextEventArgs(e.Node, e.Node.Text);
  this.OnRequestEditText(editTextArgs);

  // cancel the edit if required
  if (editTextArgs.Cancel)
    e.CancelEdit = true;

  // apply the text to the EDIT control
  if (!e.CancelEdit)
  {
    IntPtr editHandle;

    editHandle = SendMessage(this.Handle, TVM_GETEDITCONTROL, IntPtr.Zero, IntPtr.Zero); // Get the handle of the EDIT control
    if (editHandle != IntPtr.Zero)
      SendMessage(editHandle, WM_SETTEXT, IntPtr.Zero, editTextArgs.Label); // And apply the text. Simples.
  }

  base.OnBeforeLabelEdit(e);
}

And an sample usage scenario from the demo application:

csharp
private void subclassedTreeView_RequestEditText(object sender, NodeRequestEditTextEventArgs e)
{
  e.Label = e.Node.Name;
}

In this example, we are setting the edit text to be the value of the TreeNode's Name property, regardless of whatever its Text is.

Updating the text post-edit

After the conclusion of the label editing, the node's text will be set to the new label, and therefore we need to tinker that logic to allow the implementer to specify the new value text.

You could just hook the AfterLabelEdit event and have your custom logic in there (remembering to cancel the default edit), as shown here:

csharp
private void notifyTreeView_AfterLabelEdit(object sender, NodeLabelEditEventArgs e)
{
  if (e.Label != null)
  {
    e.CancelEdit = true;

    e.Node.Name = e.Label;
    e.Node.Text = string.Format("{0}: {1}", e.Label, e.Node.Tag);
  }
}

However, I didn't want to be having to do this type of code each time I implemented this sort of behaviour in an application. Rather than get fancy with subclassed TreeNode classes, I choose to add a sister event for RequestEditText, named RequestDisplayText and then handle this automatically. This is the only aspect of this article that feels "smelly" to me - ideally it would be nice if the control could handle this for you without having to ask for more information. But, this should do for the time being.

csharp
protected override void OnAfterLabelEdit(NodeLabelEditEventArgs e)
{
  if (e.Label != null) // if the user cancelled the edit this event is still raised, just with a null label
  {
    NodeRequestTextEventArgs displayTextArgs;

    displayTextArgs = new NodeRequestTextEventArgs(e.Node, e.Label);
    this.OnRequestDisplayText(displayTextArgs);

    e.CancelEdit = true; // cancel the built in operation so we can substitute our own

    if (!displayTextArgs.Cancel)
      e.Node.Text = displayTextArgs.Label;
  }

  base.OnAfterLabelEdit(e);
}

And an example of use:

csharp
private void subclassedTreeView_RequestDisplayText(object sender, NodeRequestTextEventArgs e)
{
  e.Label = string.Format("{0}: {1}", e.Label, e.Node.Tag);
}

The demonstration shows both of these approaches - the TreeViewEx control favours the RequestDisplayText event, and the TreeViewExNotify control leaves it entirely to the implementer to deal with.

Closing notes

And that's it. I've seen some implementations of this sort of functionality in various places across the internet, and some of them are pretty awful, having to override all sorts of methods, store and restore various states. The above solution is pretty simple and works regardless of if you are calling TreeNode.BeginEdit or using the "click and hover" approach on a node.

In addition, it's trivially easy to expand this to support validation as well, I'll cover that in the next article.

Bonus Chatter

I originally tried two different approaches to modifying the value, both of these involved capturing the TVN_BEGINLABELEDIT notification. The first approach used a NativeWindow bound to the TreeView control's parent watching for the WM_NOTIFY message. The second approach did the same thing, but used MFC's Message Reflection via WM_REFLECT to intercept the notification message on the tree view itself. Both of these solutions worked, and yet were still overkill as overriding OnBeforeLabelEdit is sufficient.

Although I'm not going to describe that approach here as it'll just clutter the article, I did include an implementation of the WM_REFLECT solution in the demonstration project as I think it is a neat technique and potentially useful for other notifications.

Update History

  • 2013-10-28 - First published
  • 2020-11-21 - Updated formatting

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments