devxlogo

Create Editable XML Documents Using XPath and the TreeView Control

Create Editable XML Documents Using XPath and the TreeView Control

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):

                                                 someone@some_pop_mail.net            Edinburgh            United Kingdom                                             someone@some_web_mail.net            Papakura            New Zealand                                             someone_else@some_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:

   [C#]   private void populateTreeControl(System.Xml.XmlNode document,      System.Windows.Forms.TreeNodeCollection nodes)   {      foreach (System.Xml.XmlNode node in          document.ChildNodes)      {         System.Xml.XmlNode expr =             node.SelectSingleNode(xpath_filter);                  if (expr != null)         {            TreeNode new_child = new                TreeNode(expr.Value);            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         Dim expr As System.Xml.XmlNode =  _            node.SelectSingleNode(xpath_filter)            If Not (expr Is Nothing) Then            Dim new_child As New TreeNode(expr.Value)            nodes.Add(new_child)            populateTreeControl(node, new_child.Nodes)         End If      Next   End Sub

At class-level scope add the following line:

   [C#]   private string xpath_filter =       "@id[parent::contacts or parent::contact]";   

   [VB]   Private xpath_filter As String = _      "@id[parent::contacts or parent::contact]"

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.

   attribute::id[not(parent::email or       parent::city or parent::country)]

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
Having defined what the TreeView will display, you’re now ready to tackle the problem of moving its elements around. Most developers are familiar with the general concepts involved in supporting drag-and-drop, whether they’re coding in Visual C++, Visual Basic, or one of the .NET languages. For this reason, I’ll move straight to the implementation of the four salient methods:

  • MouseDown?the user selected something
  • DragEnter?the use started dragging the selected item
  • DragOver?the user dragged the selected item over another item
  • DragDrop?the user dropped the selected item somewhere

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.

  1. How do you associate a node in the TreeView control with the matching node in the underlying XML document?
  2. How do you manipulate the XML document so that a physical transformation follows the graphical one?
  3. How do you perform both actions efficiently with large documents? If changes like this are going to be processor-intensive, you don’t want to tie up the user interface any more than necessary.

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.”

   *[attribute::id][2]/*[attribute::id][5]

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 elements.

   [0]         [0]           [1]           [2]     

However, the TreeView sees the same document as a hierarchical list of nodes.

   [0]         [0]              [0]              [1]              [2]           [1]          ... 

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:

  • Instruction: Remove node[0], child[0]; reinsert it after node[0], child[0]
  • TreeView: Remove “contact” node called “Alex”; insert it after “contact” node called “Rebekah”
  • XML document: Remove “contact” node called “Alex”; insert it after “contact” node called “Rebekah”

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:

   [0]         [0]              [0]           [1]     

In contrast the XML document would represent the nodes differently:

   [0]         [0]               [0]               [1]               [2]               [3]                  [0]                  [1]                  [2]            [1]            ...      

An instruction that makes perfect sense for the TreeView representation doesn’t necessarily work identically for the XML document:

  • Instruction: Remove node[0], child[0], child[0]
  • TreeView: Remove “contact” node called “Rebekah”
  • XML document: Incorrectly remove “email” node from contact called “Alex”

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.

   [C#]   private void XmlTreeView_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)   {      // Allow the user to drag tree nodes within a       // tree      if (e.Data.GetDataPresent(         "System.Windows.Forms.TreeNode", true ))      {         e.Effect = DragDropEffects.Move;         drag_drop_active = true;      }      else         e.Effect = DragDropEffects.None;   }   

   [VB]   Private Sub XmlTreeView_DragEnter( _      ByVal sender As Object, _      ByVal e As System.Windows.Forms.DragEventArgs) _      Handles MyBase.DragEnter      ' Allow the user to drag tree nodes within a tree      If e.Data.GetDataPresent( _         "System.Windows.Forms.TreeNode", True) Then         e.Effect = DragDropEffects.Move         drag_drop_active = True      Else         e.Effect = DragDropEffects.None      End If   End Sub

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
Most of the hard work’s over. You’ve implemented drag and drop, but for completeness it would be nice to have some basic editing facilities as well. Drawing on the code you’ve already seen, it only takes four lines to delete a folder:

   [C#]   string xpath_query =       buildXPathQuery(this.SelectedNode);   System.Xml.XmlNode node =       xml_document.DocumentElement.SelectSingleNode(      xpath_query);   node.ParentNode.RemoveChild(node);   SelectedNode.Remove();   

   [VB]   Dim xpath_query As String = _      buildXPathQuery(Me.SelectedNode)      Dim node As System.Xml.XmlNode = _         xml_document.DocumentElement.SelectSingleNode( _         xpath_query)      node.ParentNode.RemoveChild(node)      SelectedNode.Remove()

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:

   [C#]   private void XmlTreeView_AfterLabelEdit(object sender,       System.Windows.Forms.NodeLabelEditEventArgs e)   {      string xpath_query = buildXPathQuery(e.Node);      System.Xml.XmlNode node =          xml_document.DocumentElement.SelectSingleNode(         xpath_query);      System.Xml.XmlNode label =          node.SelectSingleNode(xpath_filter);      label.Value = e.Label;   }      

   [VB]   Private Sub XmlTreeView_AfterLabelEdit( _      ByVal sender As Object, _      ByVal e As       System.Windows.Forms.NodeLabelEditEventArgs) _      Handles MyBase.AfterLabelEdit         Dim xpath_query As String = buildXPathQuery(e.Node)      Dim node As System.Xml.XmlNode = _         xml_document.DocumentElement.SelectSingleNode( _         xpath_query)      Dim label As System.Xml.XmlNode =          node.SelectSingleNode(xpath_filter)      label.Value = e.Label   End Sub

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:

   [C#]   System.Xml.XmlDocument insert_fragment =       new System.Xml.XmlDocument();   insert_fragment.LoadXml(      "" +       "" +       "");         // The TreeView uses XmlInsertionNode to add    // a new folder to the tree's underlying XML    // document on request   xmlTreeView1.XmlInsertionNode =    insert_fragment.DocumentElement;   

   [VB]   Dim insert_fragment As New System.Xml.XmlDocument()   insert_fragment.LoadXml(" & _      "" & _      """ & _      "")   xmlTreeView1.XmlInsertionNode = _      insert_fragment.DocumentElement

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.

   [C#]   // First you need to clone the node template, and    // import it, because it originates from a different    // document   System.Xml.XmlNode copy_node = new_node.Clone();   System.Xml.XmlNode insert_node =       xml_document.ImportNode(copy_node, true);                     // Next locate which node should be its parent, and    // insert it   string xpath_query =       buildXPathQuery(this.SelectedNode);   System.Xml.XmlNode node =       xml_document.DocumentElement.SelectSingleNode(      xpath_query);   node.AppendChild(insert_node);                        // Finally, apply the xpath filter to determine what    // to display   System.Xml.XmlNode expr =       insert_node.SelectSingleNode(xpath_filter);   System.Windows.Forms.TreeNode new_child =       SelectedNode.Nodes.Add(expr.Value);   populateTreeControl(insert_node, new_child.Nodes);                        // Select the node, to force the tree to expand   SelectedNode = new_child;      // And start editing the new folder name   suppress_label_edit = false;   new_child.BeginEdit();      

   [VB]   ' First you need to clone the node template, and    ' import it, because it originates from a different    ' document.   Dim copy_node As System.Xml.XmlNode = new_node.Clone()   Dim insert_node As System.Xml.XmlNode = _      xml_document.ImportNode(copy_node, True)      ' Next locate which node should be its parent,    ' and insert it   Dim xpath_query As String = _      buildXPathQuery(Me.SelectedNode)   Dim node As System.Xml.XmlNode =       xml_document.DocumentElement.SelectSingleNode( _      xpath_query)   node.AppendChild(insert_node)      ' Finally, apply the xpath filter to determine what    ' should be   ' displayed   Dim expr As System.Xml.XmlNode = _      insert_node.SelectSingleNode(xpath_filter)   Dim new_child As System.Windows.Forms.TreeNode = _      SelectedNode.Nodes.Add(expr.Value)   populateTreeControl(insert_node, new_child.Nodes)      ' Select the node, to force the tree to expand   SelectedNode = new_child      ' And start editing the new folder name   suppress_label_edit = False   new_child.BeginEdit()

The TreeView Control in Action
That’s pretty much it. The two versions of Listing 4 (VB.NET version, or C# version) contain the final code for the hybrid TreeView control. I’ve restructured and tidied the code a bit to make it easier to use, and added a KeyDown handler to support some of the default accelerators, Control-N (new), F2 (edit), and DEL (delete).

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.

Attribute

Type

Parameter

Description

XmlDropTarget

Get

System.Xml.XmlNode

The XML node corresponding to the currently selected node in the TreeView

ActiveXmlDocument

Get

System.Xml.XmlDocument

The XML document bound to the TreeView. This updates as the tree changes

XPathFilter

Get; Set

string

The XPath filter used to identify the element or attribute whose value will be used to display the folder’s name. A folder constitutes the tree view’s smallest navigable unit

XmlInsertionNode

Get; Set

System.Xml.XmlNode

The template for a new folder. The TreeView caches this, and clones it when a new folder is required.

DragDropActive

Get; Set

bool

Flag denoting whether a drag/drop operation is currently in progress.

EnableEditNode

Get; Set

bool

Flag denoting whether label editing is supported (default is yes)

EnableDeleteNode

Get; Set

bool

Flag denoting whether folder deletion is supported (default is yes)

EnableInsertNode

Get; Set

bool

Flag denoting whether folder insertion is supported (default is yes)

Method: ?

Returns ?

Parameter ?

Description ?

Load

void

System.Xml.XmlDocument

Loads the specified XML document and uses it to populate the TreeView. Set XPathFilter prior to calling Load() to define an appropriate view on the underlying data.

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.

   [C#]   // Load the XML document from a file   xmlDocument = new System.Xml.XmlDataDocument();   xmlDocument.Load(strXmlDocument);               // After setting the path filter, load the    // document into the TreeView   xmlTreeView1.XPathFilter = "attribute::id";   xmlTreeView1.Load(xmlDocument);               // Defining XmlInsertionNode allows creation of new    // nodes within the TreeView   System.Xml.XmlDocument insert_fragment = new       System.Xml.XmlDocument();   insert_fragment.LoadXml("      ");   xmlTreeView1.XmlInsertionNode =       insert_fragment.DocumentElement;   

   [VB]   ' Load the XML document from a file   xmlDocument = New System.Xml.XmlDataDocument()   xmlDocument.Load(strXmlDocument)      ' After setting the path filter, load the    ' document into the TreeView   xmlTreeView1.XPathFilter = "attribute::id"   xmlTreeView1.Load(xmlDocument)      ' Defining XmlInsertionNode allows creation of new    ' nodes within the TreeView   Dim insert_fragment As New System.Xml.XmlDocument()   insert_fragment.LoadXml("" & _      "")   xmlTreeView1.XmlInsertionNode =       insert_fragment.DocumentElement

Figure 2 shows a screenshot of the catalog administrator form, and Figure 3 shows a detail of the final catalog.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist

©2024 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.