Taming the TreeView
The TreeView control provides a sophisticated infrastructure that allows it to be used in countless different ways. Each individual TreeView, however, is generally only used in a specific set of limited ways, depending on the underlying data it represents. That means that the TreeView is an ideal control for subclassing.
A Project Tree
You can easily create custom TreeView classes that are targeted for a specific type of data. Consider the ProjectTree class that follows:
public class ProjectTree : TreeView
{
// Use an enumeration to represent the three types of nodes.
// Specific numbers correspond to the database field code.
public enum StatusType
{
Unassigned = 101,
InProgress = 102,
Closed = 103
}
// Store references to the three main node branches.
private TreeNode nodeUnassigned = new TreeNode("Unassigned", 0, 0);
private TreeNode nodeInProgress = new TreeNode("In Progress", 1, 1);
private TreeNode nodeClosed = new TreeNode("Closed", 2, 2);
// Add the main level of nodes when the control is instantiated.
public ProjectTree() : base()
{
base.Nodes.Add(nodeUnassigned);
base.Nodes.Add(nodeInProgress);
base.Nodes.Add(nodeClosed);
}
// Provide a specialized method the client can use to add nodes.
public void AddProject(string name, StatusType status)
{
TreeNode nodeNew = new TreeNode(name, 3, 4);
nodeNew.Tag = status;
switch (status)
{
case StatusType.Unassigned:
nodeUnassigned.Nodes.Add(nodeNew);
break;
case StatusType.InProgress:
nodeInProgress.Nodes.Add(nodeNew);
break;
case StatusType.Closed:
nodeClosed.Nodes.Add(nodeNew);
break;
}
}
}
When you use this class in a program, you don't add nodes objects; instead, you add projects. The only variable elements for a project are the name and the status. Once your class has these two pieces of information, it can automatically add a node to the correct branch with the correct icon. (The icons are identified by numbers and only come into effect if an appropriately configured ImageList is attached to the ImageList property. This detail could be incorporated in the ProjectTree class, but it would require more work and wouldn't produce any obvious benefits.)
The client might use this custom TreeView as follows:
ProjectTree treeProjects = new ProjectTree();
treeProjects.Dock = DockStyle.Fill;
this.Controls.Add(treeProjects);
treeProjects.ImageList = imagesProjects;
DataTable dtProjects = GetAllProjects();
foreach (DataRow drProject in dtProjects.Rows)
{
treeProjects.AddProject(drProject("Name").ToString(),
drProject("Status").ToString());
}
The resulting display is shown in Figure 6-8.
Figure 6-8: A custom TreeView
The appeal of this approach is that the appropriate user interface class wraps many of the extraneous details and makes the rest of the code more readable. Depending on your application, you might want to develop a custom TreeView like this into a separate assembly you can reuse in different products.
There's no limit to the possible features you can add to a TreeView class. For example, you can add special methods for finding nodes or presenting context menus. The danger is that you will make the control too specific, locking functionality into places where it can't be reused. Remember to think of your custom TreeView as a generic TreeView designed for a specific type of data. However, it should allow many different possible uses of that data. For example, if you determine that a user action should result in a database select or update, you must raise an event from the TreeView, and allow the code receiving that event to take care of the data layer.
A Data-Aware TreeView
Another approach is to create a custom TreeView that recognizes the appropriate DataRow objects natively. When an item is selected, the custom class raises a specialized event that is more useful than the generic AfterSelect event. A different event is raised depending on the type of selected item, and the original DataRow object is returned as an argument.
public class ProjectUserTree : TreeView
{
// Use an enumeration to represent the two types of nodes.
public enum NodeType
{
Project,
User
}
// Define a new type of higher-level event for node selection.
public delegate void ItemSelectEventHandler(object sender,
ItemSelectEventArgs e);
public class ItemSelectEventArgs : EventArgs
{
public NodeType Type;
public DataRow ItemData;
}
// Define the events that use this signature and event arguments.
public event ItemSelectEventHandler UserSelect;
public event ItemSelectEventHandler ProjectSelect;
// Store references to the two main node branches.
private TreeNode nodeProjects = new TreeNode("Projects", 0, 0);
private TreeNode nodeUsers = new TreeNode("Users", 1, 1);
// Add the main level of nodes when the control is instantiated.
public ProjectUserTree() : base()
{
base.Nodes.Add(nodeProjects);
base.Nodes.Add(nodeUsers);
}
// Provide a specialized method the client can use to add projects.
// Store the corresponding DataRow.
public void AddProject(DataRow project)
{
TreeNode nodeNew = new TreeNode(project["Name"].ToString(), 2, 3);
nodeNew.Tag = project;
nodeProjects.Nodes.Add(nodeNew);
}
// Provide a specialized method the client can use to add users.
// Store the corresponding DataRow.
public void AddUser(DataRow user)
{
TreeNode nodeNew = new TreeNode(user["Name"].ToString(), 2, 3);
nodeNew.Tag = user;
nodeUsers.Nodes.Add(nodeNew);
}
// When a node is selected, retrieve the DataRow and raise the event.
protected override void OnAfterSelect(TreeViewEventArgs e)
{
base.OnAfterSelect(e);
ItemSelectEventArgs arg = new ItemSelectEventArgs();
arg.ItemData = (DataRow)e.Node.Tag;
if (e.Node.Parent == nodeProjects)
{
arg.Type = NodeType.Project;
ProjectSelect(this, arg);
}
else if (e.Node.Parent == nodeUsers)
{
arg.Type = NodeType.User;
UserSelect(this, arg);
}
}
}
This technique of intercepting events and providing more useful, higher-level events is quite helpful, and provides an easier model to program against.
Tip
Chapter 9 shows an example of how a TreeView can interact without ADO.NET data objects, without needing to understand the underlying field structure. Look for the Decoupled TreeView example toward the end of the chapter.
Unusual Trees
Another reason you might want to create a custom TreeView is to create an unusual tree like the one Windows uses for print settings (see Figure 6-9).
Figure 6-9: Windows print settings
When a node is clicked in this window, an edit control (like a text box) is provided allowing information to be added in-place. Implementing a design like this, as long as you define clear rules, is fairly straightforward. You could store a collection of controls, or even store a control in each node's Tag property. In the OnAfterSelect() method, check to see if the node has a corresponding control, and if it does, display it next to the node.
Design-Time Support for the Custom TreeView
You'll notice that your custom control class isn't added to the Toolbox. To accomplish that, you need to create a separate control project, as explained in Chapter 8. However, there's no reason that you can't use the custom TreeView class by instantiating manually in your code, as we saw earlier:
ProjectTree tree = new ProjectTree();
tree.Dock = DockStyle.Fill;
this.Controls.Add(tree);
Another approach that works well for derived controls is to create a more basic, related control, and then modify the designer code in your form so that it creates your control instead. Then, you'll find that you can work with your control at design time, even setting its properties through the Properties window, without needing to create a separate project.
In this example, you create a standard TreeView. Then, replace the following two lines (found at different places in the designer code):
private tree As System.Windows.Forms.TreeView;
this.tree = new System.Windows.Forms.TreeView();
With these lines (where CustomTreeView is the namespace of your project):
private tree As CustomTreeView.ProjectTree;
this.tree = new CustomTreeView.ProjectTree();
The tree even appears in the designer with its three main branches. Unfortunately, it also develops the nasty habit of adding its basic set of nodes twice: one at design-time, which is serialized in the form's designer code, and again at runtime. Chapter 8 explains how to code around these quirky behaviors. You can also refer to the CustomTreeView project included with the online samples.