Displaying Hierarchical Information in a TreeView Control

Displaying Hierarchical Information in a TreeView Control

Every user has seen the TreeView control in action at least once—in Windows Explorer, various file dialog boxes, the Registry Editor, or the hundreds of other applications that use the TreeView control to display hierarchical information. Using the TreeView control used to be a bit painful in the pre–.NET Framework days—in C++, the control was hard to use, and in Visual Basic, you couldn’t get to all the functionality. In the .NET Framework, however, you can keep things simple or get as fancy as the control will allow.

Designing a TreeView Control

To get a feel for what you can do with a TreeView control without writing a lot of code, you can use the Visual C# Forms Designer. With the Forms Designer, you can add items to a TreeView control, define its look and feel, and navigate the tree—from the end user’s as well as the programmer’s point of view.

Adding TreeView Items

The easiest way to start experimenting with a TreeView control is to manually add tree nodes at design time. Simply drag a TreeView control onto your form, and in the Properties window, open the Nodes collection. This will open the TreeNode Editor, shown in Figure 16-8.

Figure 16-8.
Manually editing tree nodes using the TreeNode Editor.

You can add root or child nodes, as well as delete and edit tree nodes. When you’ve finished, take a look at the InitializeComponent method in your form’s code to see what code was generated to build the tree. The code will be similar to the following:

this.tvSimple.Nodes.AddRange(new TreeNode[] 
{
    new TreeNode("Continents", new TreeNode[] 
    { 
        new TreeNode("Africa"),
        new TreeNode("America"),
        new TreeNode("Europe"),
        new TreeNode("Asia"),
        new TreeNode("Australia")
    }),
    new TreeNode("SUV\'s", new TreeNode[] 
    {
        new TreeNode("Hummer"),
        new TreeNode("Pinzgauer")
    })
});

This code has been made a little more readable than what you’ll actually see in the InitializeComponent method, but it’s the same as what the TreeNode Editor generates for you. Later examples show different ways to add tree nodes dynamically—without the help of the TreeNode Editor.

Setting Style Properties

A TreeView control can come in variety of styles. For example, it can be text only, as in the current example, or it can show images for each tree node. Table 16-1 lists some of the more important style properties for the TreeView control.

You can experiment with these properties in Design view to see how the appearance of the control changes.

Navigating the TreeView Control

You now have a basic tree, without having written a line of code yourself. When your users start the application, they have a fully functional TreeView control that they can navigate by selecting, expanding, and collapsing nodes.

In addition to users being able to navigate the tree interactively, you can control the tree programmatically. This is where the ExpandAll and CollapseAll methods come into the picture; these two methods do exactly what their names imply. To demonstrate how these methods work, the example application Simple­Treeview, on the companion CD, creates two buttons that call the methods, as shown here:

private void cmdExpandAll_Click(object sender, System.EventArgs e)
{
    tvSimple.ExpandAll();
}

private void cmdCollapseAll_Click(object sender, System.EventArgs e)
{
    tvSimple.CollapseAll();
}

These two methods always act on the entire tree. Sometimes it’s more appropriate to expand or collapse only a single branch of the tree, starting at a defined node. To do so, you can call the ExpandAll and CollapseAll methods on the currently selected node. The example application allows the user to expand or collapse the currently selected node via a shortcut menu, as shown here:

private void OnExpandNode(object sender, System.EventArgs e)
{
    TreeNode tn = tvSimple.SelectedNode;
    if (null == tn) return;
    tn.ExpandAll();
}

private void OnCollapseNode(object sender, System.EventArgs e)
{
    TreeNode tn = tvSimple.SelectedNode;
    if (null == tn) return;
    tn.Collapse();
}

Figure 16-9 shows this shortcut menu in action.

Figure 16-9.
Using a shortcut menu to enable the user to collapse or expand branches.
Generating Dynamic TreeView Controls

Most of the time, you use TreeView controls to display dynamic trees, not static trees that are fully defined at compile time, as in the previous section. Examples of dynamic trees include the file system’s hierarchical structure, an XML file that has a tree structure, and even the Windows registry. For each of these cases, you determine the nodes dynamically and then add them to the TreeView control.

Because Windows Explorer is such a natural fit to show off TreeView controls (and ListView controls, as you’ll see later in this chapter), you’ll find numerous examples of Explorer look-alikes in various books and articles addressing this topic, both on line and off line. This section breaks with tradition, using the Windows registry as the base for a dynamic TreeView control.

In addition to creating a dynamic TreeView control, we’ll also look more closely at the various classes in the Microsoft.Win32 namespace. To follow along with the procedures in the following subsections, create a new project and add a TreeView control to its main form. The completed application can be found in the directory RegistryTree on the companion CD.

Adding Node Images

Each node in a TreeView control can be in one of two states: closed or open. You can represent those two states with images—for example, using closed and open folder bitmaps as is done by Windows Explorer.

note

If you’re not an accomplished graphic artist, you’ll find some graphics to start out with at \Program Files\Microsoft Visual Studio .NET\Common7\Graphics. The samples in this chapter use the bitmaps provided in that directory, under bitmaps\Outline.

The easiest way to include such state images in a TreeView control is to add an ImageList object to the form. To do so, drag the object from the Toolbox window onto the form and in the Properties window, select the Images collection. The Image Collection Editor, shown in Figure 16-10, shows how easy it actually is.

Figure 16-10.
Using the Image Collection Editor to populate an ImageList object.

For this example, add the Closed.bmp and Open.bmp files (in that order) to the ImageList object. All that’s left to do is set the TreeView control’s ImageList property to the name of the ImageList object you just configured. With this task completed, you can start adding items to the TreeView control that refer to those state images.

Optimizing the Process of Adding Items to a TreeView Control

Whenever you add a new item to a TreeView control programmatically, the control is redrawn. This isn’t exactly efficient, and thus it’s good practice when you add multiple items to a TreeView control to do so in a block, as shown here:

tvRegistry.BeginUpdate();

    

tvRegistry.EndUpdate();

Calling BeginUpdate disables updates to the TreeView control; the updates are reenabled by calling EndUpdate. Between these two calls, you can add as many tree nodes as you want without causing rendering performance issues. The following practical example illustrates how the registry roots are added to a TreeView control:

public void RootNodes()
{
    tvRegistry.BeginUpdate();

    TreeNode tnHKCR = new TreeNode("HKEY_CLASSES_ROOT",0,1);
    tvRegistry.Nodes.Add(tnHKCR);
    AddBranch(tnHKCR);
    
    

    tvRegistry.SelectedNode = tnHKLM;
    tvRegistry.EndUpdate();
}

In total, the RootNodes method adds five registry roots to the TreeView control. For the sake of clarity, only one registry root is included in this code snippet. The calls to BeginUpdate and EndUpdate surround all TreeView-related work the method is doing—namely, creating a new TreeNode object and adding it to the Nodes collection of the TreeView object. By passing the name of the node and the index of the closed state as well as the index of the open state to the constructor, there’s no need to set additional properties.

The AddBranch method is used to populate the next level in the branch. There’s one good reason for doing this: prepopulating the entire tree would be too time-consuming. Therefore, we populate the tree as we go or, to be more precise, as the user navigates through the tree.

Handling TreeView Events

Because we’ve decided to populate the tree just in time—that is, when the user decides to expand a branch of the tree—only one event exactly fits the bill. Before we focus on this event, take a look at Table 16-2, which lists all the available TreeView events.

Table 16-2.  TreeView Events

Event

Description

AfterCheck

Fires after the node check box has been selected

AfterCollapse

Fires after a node has been collapsed

AfterExpand

Fires after a node has been expanded

AfterLabelEdit

Fires after the node label has been edited

AfterSelect

Fires after a node has been selected

BeforeCheck

Fires before the node check box is selected

BeforeCollapse

Fires before a node is collapsed

BeforeExpand

Fires before a node is expanded

BeforeLabelEdit

Fires before a label is edited

BeforeSelect

Fires before a node is selected

ItemDrag

Fires when an item is dragged onto the TreeView control

Of these events, the most important are AfterCollapse, AfterExpand, AfterSelect, BeforeCollapse, BeforeExpand, and BeforeSelect.

The event we’ll be using to populate the tree just in time is BeforeExpand, which does some work just before the node is expanded for the user. The work that needs to be done is to create a list of all subkeys of the registry key that’s going to be expanded. To do this, we create a new event handler for Before­Expand and add the following code:

private void tvRegistry_BeforeExpand(object sender, 
                                     TreeViewCancelEventArgs e)
{
    tvRegistry.BeginUpdate();
    foreach (TreeNode tn in e.Node.Nodes)
    {
        AddBranch(tn);
    }
    tvRegistry.EndUpdate();
}

At first glance, you might think that this code is just BeginUpdate, AddBranch, and EndUpdate and that you’ve seen all this before. But if you look more closely, one item in particular should spark your interest: e.Node.Nodes. We aren’t just expanding on demand what the user wants to see. Instead, we’re looking ahead one level, to a level the user doesn’t yet see—all the user sees are the plus signs for expanding the nodes. If we don’t expand one level ahead of the user, there won’t be any plus signs, and no way for the user to drill any deeper!

One important method remains to be discussed—the often-called AddBranch method. The full code for the AddBranch method is shown here:

public void AddBranch(TreeNode tn)
{
    tn.Nodes.Clear();
    string strRegistryPath = tn.FullPath;

    RegistryKey regBranch = null;
    if (strRegistryPath.StartsWith("HKEY_CLASSES_ROOT"))
        regBranch = Registry.ClassesRoot;
    else if (strRegistryPath.StartsWith("HKEY_CURRENT_USER"))
        regBranch = Registry.CurrentUser;
    else if (strRegistryPath.StartsWith("HKEY_LOCAL_MACHINE"))
        regBranch = Registry.LocalMachine;
    else if (strRegistryPath.StartsWith("HKEY_USERS"))
        regBranch = Registry.Users;

    RegistryKey regSubKey = null;
    try
    {
        if (null != tn.Parent)
        {
            int nPosPathSeparator =
                strRegistryPath.IndexOf(this.tvRegistry.PathSeparator);
            string strSubkey =
                strRegistryPath.Substring(nPosPathSeparator + 1);
            regSubKey = regBranch.OpenSubKey(strSubkey);
        }
        else
            regSubKey = regBranch;
    }
    catch
    {
        return;
    }

    string[] astrSubkeyNames = regSubKey.GetSubKeyNames();
    for (int i=0; i < astrSubkeyNames.Length; i++)
    {
        TreeNode tnBranch = new TreeNode(astrSubkeyNames[i],0,1);
        tn.Nodes.Add(tnBranch);
    }
}

You can determine each node’s position in the tree by looking at the node’s FullPath property. In this example, the FullPath property is used to identify which portion of the registry is being expanded, and thus which root RegistryKey object must be used.

After obtaining the root RegistryKey object, the subkey is opened. Because FullPath includes the root key, it must be removed from the subkey string first, before calling OpenSubKey on the root RegistryKey. Everything is encapsulated in a try catch block in case the user doesn’t have enough permissions to open the intended branch.

Once the subkey is open and all the subkey names have been obtained using GetSubKeyNames, those names are added to the TreeView control. Remember that the callers of AddBranch are responsible for providing performance by wrapping the call in BeginUpdate and EndUpdate methods.

Implementing Your Own TreeView Control

When you’re working on a larger-scale project, reusing components, or componentization in general, becomes more important. In the context of our TreeView example, this means that the TreeView control itself should know how to read the registry, should have no need for an external ImageList object, and should provide its own tree navigation. To accomplish this, you’ll need to derive your own customized TreeView class.

Creating a TreeView-Derived Class

The intention of this exercise is to demonstrate how easily you can encapsulate functionality in your own control-derived classes—there’s really no good excuse for not doing it. You can find the source code for this entire example in the RegistryTreeWithClass directory on the companion CD.

The first step is of course to open a new project and, within that project, add a new empty source file. In this file, you’ll add your new RegistryTreeClass class, which is derived from TreeView. Add a constructor where you’re going to place the ImageList object loading code, and override the OnBeforeExpand method, which handles the BeforeExpand method internally. That’s all the new code you’ll need; the rest of the code is reused from the preceding example. The result is shown here:

using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Microsoft.Win32;

namespace RegistryTreeWithClass
{
    public class RegistryTreeClass: TreeView
    {
        public RegistryTreeClass()
        {
            ImageList = new ImageList();
            // Both images are embedded resources.
            ImageList.Images.Add(new Bitmap(GetType(), "CLOSED.BMP"));
            ImageList.Images.Add(new Bitmap(GetType(), "OPEN.BMP"));
            RootNodes();
        }

        protected override void OnBeforeExpand(TreeViewCancelEventArgs e)
        {
            base.OnBeforeExpand(e);
            BeginUpdate();

            foreach (TreeNode tn in e.Node.Nodes)
            {
                AddBranch(tn);
            }
            EndUpdate();
        }
        
        public void RootNodes()
        {
            BeginUpdate();

            TreeNode tnHKCR = new TreeNode("HKEY_CLASSES_ROOT", 0, 1);
            Nodes.Add(tnHKCR);
            AddBranch(tnHKCR);

            
    


            SelectedNode = tnHKLM;
            EndUpdate();
        }

        public void AddBranch(TreeNode tn)
        {
            
    

        }
    }
}

The ellipses represent code that hasn’t changed. In the constructor, you create the ImageList object and then load the images that are defined as embedded resources for the project. (The images are defined as an embedded resource by choosing Add Existing Item from the Project menu to add the image to your project and then setting the image’s Build Action property to Embedded Resource.) The call to RootNodes completes the work done in the constructor.

In addition to no longer having to reference a control instance (tvRegistry is gone), the only important action to take is in OnBeforeExpand: don’t forget to call the base class implementation. The resulting code is virtually the same as before, but it’s embedded in a class waiting to be used in an application.

Using the RegistryTreeClass Class

The RegistryTreeClass class we just created isn’t a perfect citizen in the component world, but it’s a good one. The class isn’t yet ready to be used in the Forms Designer, but Forms Designer can be used to add a TreeView control to your form. You can then simply change the class type in the declaration of the variable, as shown here:

public class MainForm : System.Windows.Forms.Form
{
    private RegistryTreeClass rtvRegistry;

Usually, you’ll find System.Windows.Forms.TreeView used as a class type, but here it’s been changed to RegistryTreeClass. Compile and run the application; the results will be similar to Figure 16-11.

Figure 16-11.
The RegistryTreeClass class in action.

Code reuse is important; we’ll return to this topic later in this chapter, in the section “RegistryViewer—TreeView and ListView Combined.”



Part III: Programming Windows Forms