ecently, I was working on the user interface for a tool to maintain an online catalog. Because the catalog featured so many products, it made sense to categorize them in some way. The catalog administrator would need the ability to delete and define new categories, nest categories within other categories, and generally manipulate both categories and products in a direct and intuitive fashion.
Hierarchical scenarios like this cry out for a hierarchical view of some kind. First, the mapping between data and its representation is usually trivial, because the TreeView control’s object model is itself hierarchical. Second, the ability to expand a tree’s nodes on an individual basis makes it easy to browse data at multiple levels and granularities simultaneously. Finally, dragging and dropping folders in a TreeView is a particularly simple and appealing way to manipulate complex hierarchies quickly.
After a few minutes I realized that the application I had in mind was Windows Explorer, and that I was about to rewrite it, with product categories in place of file folders and product items in place of the files themselves. I was even going to re-implement the accelerator shortcuts to create and delete folders and perform drag-and-drop operations. And I would probably find myself writing the same thing all over again if I later decided to code an interface to a relational database, a contacts management program, or even a tool to trace my genealogy.
This made no sense. What I needed was a generic way to bind a hierarchical data source to a TreeView control, in the same way that you can bind a database table to a data grid. But in this case I wanted to get all the creation, deletion, renaming, moving, and dragging-and-dropping of data elements “for free,” regardless of the structural content of the data source in question.
Binding an XML Document to a TreeView Control
With its hierarchical structure, XML was the logical choice of data format. You can display an XML document in a TreeView control in fewer than half-dozen lines of code.
Suppose you have an XML document containing contacts like this (contacts.xml):
[email protected]_pop_mail.net Edinburgh United Kingdom [email protected]_web_mail.net Papakura New Zealand [email protected]_web_mail.com Muriwai New Zealand
You can easily iterate recursively through all its elements to populate the TreeView control, adding the XML nodes to the TreeView’s TreeNodeCollection while maintaining the same node relationship that exists in the XML document.
[C#] private void populateTreeControl( System.Xml.XmlNode document, System.Windows.Forms.TreeNodeCollection nodes) { foreach (System.Xml.XmlNode node in document.ChildNodes) { // If the element has a value, display it; // otherwise display the first attribute // (if there is one) or the element name // (if there isn't) string text = (node.Value != null ? node.Value : (node.Attributes != null && node.Attributes.Count > 0) ? node.Attributes[0].Value : node.Name); TreeNode new_child = new TreeNode(text); nodes.Add(new_child); populateTreeControl(node, new_child.Nodes); } }
[VB] Private Sub populateTreeControl( _ ByVal document As System.Xml.XmlNode, _ ByVal nodes As _ System.Windows.Forms.TreeNodeCollection) Dim node As System.Xml.XmlNode For Each node In document.ChildNodes ' If the element has a value, display it; ' otherwise display the first attribute ' (if there is one) or the element name ' (if there isn't) Dim [text] As String If node.Value <> Nothing Then [text] = node.Value Else If Not node.Attributes Is Nothing And _ node.Attributes.Count > 0 Then [text] = node.Attributes(0).Value Else [text] = node.Name End If End If Dim new_child As New TreeNode([text]) nodes.Add(new_child) populateTreeControl(node, new_child.Nodes) Next node End Sub
Now, you can create a new Windows Forms application, drop a TreeView control onto the form, and add the following three lines to your form’s constructor:
[C#] System.Xml.XmlDocument document = new System.Xml.XmlDataDocument(); document.Load("../../contacts.xml"); populateTreeControl(document.DocumentElement, treeView1.Nodes);
[VB] Dim document As New System.Xml.XmlDataDocument() document.Load("../contacts.xml") populateTreeControl(document.DocumentElement, _ TreeView1.Nodes)
When you expand the TreeView nodes, you should see something like Figure 1.
![]() |
Filtering XML Data Let’s take a step back. If this were a real contacts application, users wouldn’t expect to see “email,” “city,” or “country” in the hierarchical view. Rather, they would expect some top-level identification of who the contact is?Alex, Rebekah, or Justin, in this case?and the set of details associated with that contact (email, city) in an adjoining, editable pane. Similarly, users are likely to want to drag contacts up and down the tree to rearrange them; however, dragging email addresses or cities between individual contacts, which the use of a TreeView control implies would also be possible, makes no sense. That’s because people normally expect a hierarchical view to summarize data, not just categorize it. In other words, there are valid reasons to change contact order or groupings, but “city” and “country” elements are clearly associated with specific contacts, and you’d no more expect to find them in isolation than to find pairs of them together. One solution is to hide the sub-fields when you display the contact tree. For example, you could add a special attribute (such as view=”hide”) to sub-elements such as email and address that you don’t want to display. Then, you could ignore any elements (and their children) containing that special attribute in the populateTreeControl method when populating the TreeView control. Although that would work, changing the data source to suit the data consumer isn’t a responsible design decision. A better approach is for the data consumer to define what structures the hierarchical view can or cannot manipulate within a given document. You can achieve this by modifying the populateTreeControl() method so it supports XPath queries. For example:
At class-level scope add the following line:
You use the result of the XPath query to determine whether to recurse into its children. In this case, the query constitutes an inclusion rule, reading “Select the id attribute of any ‘contacts’ or ‘contact’ element.” But you might equally well use an exclusion rule to identify the data you explicitly want to reject.
This isn’t a generic solution, but using a filter based?like this one?on parent-child relationships, rather than on unqualified node or attribute pattern matches, is an effective way to express the fundamental structures of an XML document when users have substantial editorial rights with respect to its hierarchy. In this case, a simple query was sufficient, but there’s no reason you couldn’t go for something more complicated. Implementing Drag and Drop
Implementing these methods appropriately gives users real-time visual feedback on the items that can and can’t be manipulated, how they can be manipulated, and whether a particular manipulation is possible in a given context. As a result, there are three immediate problems that need to be addressed.
Listing 1 shows the MouseDown handler and the helper method buildXPathQuery that it calls. First the code checks that a node is selected. Next, it stores both the TreeNode (drag_node) and the XPath query that relates it to the root of its underlying XML document (xpath_remove_query), using the filter defined earlier. For example, the following query identifies the fifth child folder of the second child folder from the root of the tree, where a folder can be uniquely identified with the query “attribute::id.”
The code in Listing 1 provides sufficient information to remove both the TreeNode and its associated XmlNode unambiguously when a user drags a node to a different location. You might think you could get the same effect without referring to the filter at all, and simply specify something like “Drop the second child from the document root inside its first child.” But there’s a gotcha here. It’s the filter that enforces the one-to-one mapping between the TreeView’s node hierarchy and that of the XML document; without it, such directions can be ambiguous. For example, suppose the filter matches the following structure
The restriction means the XPath filter sees the hierarchy of contacts.xml as a simple list of child
However, the TreeView sees the same document as a hierarchical list of nodes.
So long as contact entries never get nested within one another, you can keep the TreeView control and its underlying XML document synchronized without recourse to the filter. For example, if you wanted to swap the “Alex” and “Rebekah” contact entries, you could easily do so:
But with nested contacts, the same instruction results in a misalignment between the TreeView representation and the XML document representation. For example, suppose you tried to remove the nested “Rebekah” contact in the following TreeView representation:
In contrast the XML document would represent the nodes differently:
An instruction that makes perfect sense for the TreeView representation doesn’t necessarily work identically for the XML document:
With the help of a filter that can distinguish contacts as discrete entities rather than simply the path to some particular node in a tree, you no longer need to worry where exactly the child contact “Rebekah” sits within the internal structure of its parent “Alex” contact, and accordingly you safeguard yourself from situations like this. Assume a user decides to drag one of the contacts. The next step is to give feedback on what the user is doing. A trivial implementation of DragEnter checks to ensure that the item being dragged is a TreeView node of some kind, and then records the fact a drag-drop operation has begun. This is largely for the benefit of the control’s hosting application, which may want to take action of its own at this point. Accordingly, the variable drag_drop_active is directly exposed elsewhere as the attribute DragDropActive.
The DragOver method that follows (see Listing 2) gets called continuously as users drag the folder, so, for reasons of efficiency, the code first checks whether the dragged folder has changed since the last DragOver call. If so, it then checks to determine what style of drag is in progress. Since I needed to allow an end user to rearrange both the order and the hierarchy of folders, I elected here to follow familiar Windows behavior (so far as it was defined), and use my intuition where it wasn’t. Therefore this implementation lets users drag and copy folders that aren’t immediate siblings with the left mouse button, and use the right mouse button for intra-folder manipulations. However, implementing this presented a slight problem, because the two drags would have to operate in different ways: A left drag operation shouldn’t do anything until the drag operation is complete, but right dragging should give continuous feedback, even if, once again, no physical change to the document takes place until the user releases the mouse button. In this case, the code checks whether the node being dragged is a sibling of the node it’s being dragged over. If so, all the parent’s immediate children are detached from the tree, and then added back again, with the “drag” node and “drop” node positions swapped. As a result, the underlying data source and its visual representation get out of sync until the drop is confirmed, at which point the data source updates to reflect the visual hierarchy currently displayed. The advantage of doing it this way, is that the display updates continuously, so that users get immediate feedback about the drag operation, but the XML document needs to get updated only once, when the drag completes. Left-mouse drag-drop operations require no special code; the drag/drop API takes care of any appropriate feedback. Users complete a drag-drop operation by releasing the mouse button (see Listing 3). At that point, the sample code removes the tree node and its corresponding folder in the document, and creates an appropriate XPath query to reinsert the folder at its new location. There is one special case?when users try to insert a folder beneath a folder with no existing children,?but when that occurs, you can just create a new child. Implementing Delete, Rename, and Insert Operations
Renaming a folder takes a little more thought. While you can call buildXPathQuery to find out which folder to edit, how do you know which attribute, element or sub-element of the returned structure corresponds to that folder’s display name? It’s a trick question. That’s precisely the function of the XPath filter defined earlier. Rather elegantly, you can just apply the transforms one after the other:
The final challenge lies in working out how to create a new folder on demand. The query filter allows users to navigate the XML document in terms of folders rather than XML elements, which is helpful when you want to move those folders around. But while it can tell you where to insert a folder, it can’t tell you anything about what that folder should contain. You could surmise something if all the folders within a document had the same structure and contents, but the goal is to create a method for displaying XML that requires no such prerequisites. Accordingly, I’ve chosen to delegate the responsibility for this to the application hosting the control. This way, for example, a client could write code such as:
The TreeView control can cache a copy of this structure and use it as a template for building new folders on request. Your only responsibility is to ensure the folder is defined in such a way that the filter query identifies it as a folder. Otherwise, the TreeView control wouldn’t display it, and to all intents and purposes, it would look as if it didn’t exist. Because the XML generated above is external to the document we’re manipulating, it’s necessary to import it into the document before using it.
The TreeView Control in Action There didn’t seem any need to raise additional custom events; accordingly, the final API is a single method and eight attributes, which you’ll find listed in Table 1. The majority of these are simply flags, and switched on by default; I added them to let you fine-tune your choice of the editing features your application will support. Table 1. XmlTreeView Control Attributes and Methods: All you need to add drag-and-drop support to your TreeView applications are these eight attributes and one method.
Load the sample project that accompanies this article to see the hybrid TreeView control in action. The sample project is a cut-down version of the catalog administration utility I mentioned earlier. It’s worth mentioning is that it takes only four lines of code to implement full drag-drop support, and a further three to let users create new folders.
Figure 2 shows a screenshot of the catalog administrator form, and Figure 3 shows a detail of the final catalog.
|