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:
- MouseDownthe user selected something
- DragEnterthe use started dragging the selected item
- DragOverthe user dragged the selected item over another item
- DragDropthe 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.
- How do you associate a node in the TreeView control with the matching node in the underlying XML document?
- How do you manipulate the XML document so that a physical transformation follows the graphical one?
- 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.
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:
- Instruction: Remove node, child; reinsert it after node, child
- 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:
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:
- Instruction: Remove node, child, child
- 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
private void XmlTreeView_DragEnter(object sender, System.Windows.Forms.DragEventArgs e)
// Allow the user to drag tree nodes within a
"System.Windows.Forms.TreeNode", true ))
e.Effect = DragDropEffects.Move;
drag_drop_active = true;
e.Effect = DragDropEffects.None;
Private Sub XmlTreeView_DragEnter( _
ByVal sender As Object, _
ByVal e As System.Windows.Forms.DragEventArgs) _
' 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
e.Effect = DragDropEffects.None
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 casewhen users try to insert a folder beneath a folder with no existing children,but when that occurs, you can just create a new child.