Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adding Drag and Drop to an Explorer Tree Control

0.00/5 (No votes)
17 May 2012 4  
ExpTree part 2: Adding drag and drop to the ExpTree control.

Tbale of contents

Introduction

It seems simple to add drag and drop to a control that represents the file system. It is not simple at all, even for normal File System Objects. If your control extends beyond the file system to include all the objects in the Shell namespace, it is even more complex. The code presented here does not do the complete job, but it does handle almost all of it. All file system objects may be dropped on any folder that will accept them, including virtual folders. Full support is provided for move, copy, and "Create Shortcut(s) here" in the same familiar Windows fashion, with full support for right and left button drag, keyboard modifiers, and default actions. The code gets normal Windows behavior on drop by having Windows provide it. The classes introduced here act as a broker between the drag, its IDataObject, and the target folder's IDropTarget interface, which provides the Windows behavior, just as with Win Explorer.

Background

In part 1, An All VB.NET Explorer Tree Control with ImageList Management, I introduced a library of classes which includes a Windows Explorer-like TreeView control. That control provides a view of the Shell namespace's objects much like Win Explorer. The library also includes a full featured class that replaces the .NET file and directory classes and handles virtual folders. Also included is a class that provides painless access to the icons Windows uses to represent files, folders, and other objects as displayed by Windows Explorer. In part 2, I add drag and drop functionality to that control.

This addition provides the necessary drag/drop features.

  • Gives visual feedback to the user as the drag enters, moves over, leaves, or drops onto the control.
  • Performs the drop action.
  • Updates the control to reflect changes caused by the drop.

I assume that the readers of this article have read part 1. It would be especially helpful to understand the role of the CShItem class as presented there. Full explanation of CShItem awaits part 3, which I don't plan to write unless there is some demand for it.

What's so hard about drag and drop, and what does this code not do?

At first look, .NET provides some very good tools to support drag and drop. The .NET DataObject accepts drag items in lots of formats, and the File and Directory classes allow for File.Move, File.Copy, and Directory.Move, so what's the problem? That would be another article, but consider, there is no Directory.Copy, there is no way to create "Shortcut(s) Here", the File and Directory classes only deal with the file system, right button support means intercepting the drop and the ContextMenu always returns after your DragDrop event handler exits, and of course, there is the handling of read-only, system, and pre-existing files. If you interact with the user to deal with these cases, you should consider the locale of the system, so the user can understand what you are saying... The list goes on. There are workarounds for many of these problems. However, the technique presented here simply passes most of the problems on to the IDropTarget interface of the Shell folder being dragged over or dropped onto. The interface deals with them all at no cost to you ... well not too much cost.

The classes presented here will not correctly deal with a drag of some non-file system objects from a drag source that does not include the necessary data formats. Specifically, .NET's DataObject cannot handle some of those formats, so a .NET drag source, either in the same or a different .NET application as the control, will not provide them. Strictly speaking, this is not a problem of my classes, it is a problem of the drag source. My classes will accept pretty much anything that can be dragged from Windows Explorer - just not everything that could be dragged from a .NET source. Unfortunately, this includes dragging from the control itself. The worst case scenario involves dragging an item whose path is "http://localhost/somedirectory/somefile.htm" which comes from my network places. On the other hand, an item whose path is "\\somemachine\somedirectory\somefile.xyz" works just fine, even from my network places. Some of the reasons for this will be covered later.

Overview

The ExpTree control is a UserControl which consists solely of a TreeView which looks like the left pane of Windows Explorer. Like any control, it may be placed on a form. See Part 1 for details. To activate its ability to accept drops, set its AllowDrop property to True. On initialization, the control checks for this and, if True, creates an instance of the TVDragWrapper class and calls the API routine RegisterDragDrop to register that instance as the DragDropHandler for the TreeView. The TVDragWrapper instance will now receive and handle the TreeView's DragEnter, DragOver, DragLeave, and DragDrop events. No, .NET style drag related event handlers are required.

There is a division of labor between TVDragWrapper and ExpTree. TVDragWrapper deals with the mechanics of the drag/drop and brokering between the IDataObject being dragged and the IDropTarget interface of the DropTarget. ExpTree deals with managing the appearance of the TreeView as the drag continues. Communication with ExpTree is done via four events declared in TVDragWrapper: ShDragEnter, ShDragOver, ShDragLeave, and ShDragDrop.

The sequence of TVDragWrapper events during the drag is:

  • DragEnter is raised as the drag enters the control. The handler extracts information about what is being dragged. If acceptable, it checks for the presence of Shell's IDList array data. If there is none, it creates it and adds it to the IDataObject being dragged. If all is well, it raises the ShDragEnter event to let the TreeView know what is going on. Much of the dirty work is done by the CProcDataObject class, discussed later.
  • DragOver is raised many times as the drag moves over the surface of the control. To minimize processing, the class remembers the last TreeNode that the drag was over. If the drag is not over a node, clear the remembered state and exit. If over the same node, exit. If over a new node that is a valid DropTarget, get the IDropTarget interface of the folder the node represents, call that interface's DragEnter and DragOver events, saving the DragDropEffect the folder returns, raises the ShDragOver event for the TreeView, and exits, reporting the DragDropEffect to the Shell's DragDrop processor which will provide the user feedback.
  • DragLeave is raised if the drag leaves the control. TVDragWrapper does require cleanup and raises the ShDragLeave event so that the control can do the same.
  • DragDrop is raised when the drag drops its IDataObject. Since the earlier events have set everything up, TVDragWrapper has little to do. After error checking, it calls the IDropTarget's DragDrop method, passing in the IDataObject. It then raises the ShDragDrop event to inform the control that the drop has occurred.

The sequence of ExpTree events during the drag is:

  • ShDragEnter does nothing.
  • ShDragOver gives the user visual clues by changing the background color of the current TreeNode, and changing it back when the drag moves somewhere else. It also starts and stops a Timer which will expand a collapsed node if the drag hovers over a node for a short period of time (1200 ms currently).
  • ShDragLeave stops the Timer and resets any changed background color.
  • ShDragDrop stops the Timer, resets node background colors, and, if appropriate, calls RefreshNode on both the source and target nodes of the drop. If the target of the drop is the currently selected node of the TreeView, it also fakes a re-select of that node to inform any listeners of the ExpTreeNodeSelected event that the contents may have changed.

The devil is in the details

There are three main aspects of drag and drop: the IdropTarget interface, the IDataObject interface and reflecting changes in the application display. Each has its own set of details.

The IDropTarget interface

The TVDragWrapper class is registered as the COM IDropTarget for the control. This is done at control initialization. A small event handler handles the tv1.HandleDestroyed event to call the API routine RevokeDragDrop to clean up as the control is being destroyed. This is not the standard .NET drag/drop interface. The information received by the various event handlers of the class is similar to, but not the same as the corresponding .NET event handlers.

TVDragWrapper, as described above, receives all event notifications related to the drag. As the drag moves over the control, DragOver is repeatedly called. The following code fragment is from DragOver. It is executed once it has been determined that the drag is currently over a new node, known as tn in the code:

'Drag is now over a new node with new capabilities
    Dim CSI As CShItem = tn.Tag
    If CSI.IsDropTarget Then
        m_LastTarget = CSI.GetDropTargetOf(m_View)
        If Not IsNothing(m_LastTarget) Then
            pdwEffect = m_Original_Effect
            Dim res As Integer = _
                m_LastTarget.DragEnter(m_DragDataObj, _
                             grfKeyState, pt, pdwEffect)
            If res = 0 Then
                res = m_LastTarget.DragOver(grfKeyState, _
                                             pt, pdwEffect)
            End If
            If res <> 0 Then
                Marshal.ThrowExceptionForHR(res)
            End If
        Else
            pdwEffect = 0 'couldn't get IDropTarget, 
                          'so report effect None
        End If
    Else
        pdwEffect = 0   'CSI not a drop target, 
                        'so report effect None
    End If
    RaiseEvent ShDragOver(tn, ptClient, _
                        grfKeyState, pdwEffect)
End If
Return 0

Given the information already present in the CShItem class, it was reasonably easy to implement the GetDropTargetOf method (see source download). It returns the IDropTarget COM interface of the folder that the current node represents. Having and using the folder's IDropTarget eliminates much of the difficulties mentioned in the section "What's so hard about drag and drop" above. Note: DragOver is a method of the IDropTarget of the control. In this code it obtains the IDropTarget of the folder. Assuming that we actually have the folder's IDropTarget, we now interact with it by calling its DragEnter and DragOver methods. The folder itself provides the definitive word as to what kind of drop it will support with the dragged data. This is done via the ByRef parameter pdweffect which is exactly equivalent to .NET's DragDropEffects enumeration.

The IDataObject interface

The IDropTarget interface is actually easy to work with and gives great advantages. The IDataObject interface is much harder and gives only one reward ... without it there is no data to drop. The code deals with three flavors of IDataObject. The .NET IDataObject is peculiar to .NET applications. It is different from the "normal" COM IDataObject such as you will get if you drag from Windows Explorer. The third variation is a .NET IDataObject that is dragged from a .NET application other than the one the drag is over.

The .NET DataObject and the .NET IDataObject that it wraps is easy to work with, but suffers a fatal flaw. It cannot provide all the required data formats! Most (or all) folders that are really namespace extensions, and therefore not part of the file system, require data in the FileContents and FileGroupDescriptorW formats. These formats are defined to support multiple items per drag and have an index value to be used in getting the data. .NET's IDataObject has no provision for an index. In Framework 1.0 and 1.1 it is impossible to drag items from such folders, by standard methods, using the .NET IDataObject. I do not know for sure, but have reason to believe, that Framework 2.0 will address this problem.

The COM IDataObject is a defined interface (see MSDN for details) used to support generalized exchange of data between COM entities. My code will accept either a .NET IDataObject or a COM IDataObject as the data being dragged and dropped.

The CProcDataObject class

The CProcDataObject class does all the work in decoding the drag's IDataObject and determining its validity for the control. Its constructor accepts an IntPtr to some kind of IDataObject. It determines what kind of IDataObject it has. It then determines if the data being dragged meets certain criteria, builds an ArrayList of CShItems representing the dragged items, ensures that the IDataObject has Shell IDList Array formatted data, and sets a property to True if all has gone well.

The dragged IDataObject must have data in one or more of the following formats:

  • An ArrayList of CShItems - Only possible from within the same application instance as the control. This is the preferred format, when possible.
  • A Shell IDList Array - Preferred for drags originating outside of the application instance of the control. The MakeShellIDArray routine is publicly available to create this when a control is the source of a drag. If none is present in the IDataObject, CProcDataObject will attempt to create one.
  • FileDrop format. The last choice, for good reasons, but also the easiest format for .NET applications to make.

The ArrayList of CShItems that this class produces, saves, and provides as a property if used to determine how the GUI should be updated when the drop is performed.

Whose IDataObject is this, anyhow

The constructor of CProcDataObject figures out the kind of IDataObjects it is dealing with. First, be aware that the DragEnter and DragDrop methods of TVDragWrapper do not get a nice .NET DataObject. They receive an IntPtr which points to a COM interface which may be a .NET IDataObject or something else.

Sub New(ByRef pDataObj As IntPtr)  'Assumed to be a 
                                   'pointer to an 
                                   'IDataObject
    m_DataObject = pDataObj
    Dim HadError As Boolean = False 'used for various 
                                    'error conditions
    Try
        IDO = Marshal.GetTypedObjectForIUnknown(pDataObj, _
                             GetType(ShellDll.IDataObject))
    Catch ex As Exception
        HadError = True
    End Try
    'If it is really a .Net IDataObject, 
    'then treat it as such
    If HadError Then
        Try
            NetIDO = Marshal.GetTypedObjectForIUnknown(pDataObj, _
                         GetType(System.Windows.Forms.IDataObject))
            IsNet = True
        Catch
            IsNet = False
        End Try
    End If
    If IsNet Then
        'Any error in ProcessNetDataObject will leave 
        'm_IsValid as False -- our only Error Indicator
        ProcessNetDataObject(NetIDO)
    Else    'IDataObject not from Net, Do it the hard way
        If HadError Then Exit Sub 'can do no more
        ProcessCOMIDataObject(IDO)
        'It either worked or not.  m_IsValid is 
        'set accordingly, so we are done
    End If
End Sub

This code determines the kind of IDataObject that we are dealing with. It then calls other routines to do something useful. What is not obvious is that an IDataObject dragged from another .NET application will end up taking the ProcessCOMIDataObject path.

What is a Shell IDList Array and why do I care?

A Shell IDList Array is another name for a CIDA structure. It is important for two reasons:

  • It is the preferred way of representing Shell objects in a drag.
  • If a right button drop is performed on a folder, and there is no CIDA, the folder assumes that your link option is "Create Document Shortcut". This is never the right choice for ExpTree which wants the link option to be "Create Shortcut(s) Here". Therefore, CProcDataObject ensures that the dragged IDataObject has a Shell IDList Array. If one is not present, it makes one and adds it to the IDataObject.

The Shell IDList Array is a VB hostile structure. It is vital to the control, and it is difficult to work with. CProcDataObject provides three CIDA related routines to deal with it. They are: MakeShellIDArray to create one from an ArrayList of CShItems, MakeDragListFromCIDA which creates an ArrayList of CShItems from a CIDA represented as a MemoryStream, and MakeStreamFromCIDA which takes an IntPtr pointing to a CIDA and returns a MemoryStream. This last one is needed since the IDataObject.GetData returns the CIDA in the form of an IntPtr pointing to an IntPtr which points to the actual CIDA. The following code is one of these routines and illustrates the joy of working with this all-important data structure:

Private Function MakeStreamFromCIDA(ByVal ptr As IntPtr) As MemoryStream
    MakeStreamFromCIDA = Nothing    'assume failure
    If ptr.Equals(IntPtr.Zero) Then Exit Function
    Dim nrItems As Integer = Marshal.ReadInt32(ptr, 0)
    If Not (nrItems > 0) Then Exit Function
    Dim offsets(nrItems) As Integer
    Dim curB As Integer = 4 'already read first 4
    Dim i As Integer
    For i = 0 To nrItems
        offsets(i) = Marshal.ReadInt32(ptr, curB)
        curB += 4
    Next
    Dim pidlLen As Integer
    Dim pidlobjs(nrItems) As Object
    For i = 0 To nrItems
        Dim ipt As New IntPtr(ptr.ToInt32 + offsets(i))
        Dim cp As New cPidl(ipt)
        pidlobjs(i) = cp.PidlBytes
        pidlLen += CType(pidlobjs(i), Byte()).Length
    Next
    MakeStreamFromCIDA = New MemoryStream(_
                    pidlLen + (4 * offsets.Length) + 4)
    Dim BW As New BinaryWriter(MakeStreamFromCIDA)
    With BW
        .Write(nrItems)
        For i = 0 To nrItems
            .Write(offsets(i))
        Next
        For i = 0 To nrItems
            .Write(CType(pidlobjs(i), Byte()))
        Next
    End With
    MakeStreamFromCIDA.Seek(0, SeekOrigin.Begin)
End Function

I forgot to mention that the CIDA represents items by their PIDL. Fortunately, the CShItem class is based on Shell folders and PIDLs and contains a handy class, cPidl, for representing PIDLs as Byte().

Showing the results of the drop

Given the work done by DragEnter and DragOver, doing the actual drop in DragDrop is quite simple:

Public Function DragDrop(ByVal pDataObj As IntPtr, _
                     ByVal pt As POINT, _
                     ByRef pdwEffect As Integer) As Integer _
                     Implements IDropTarget.DragDrop
    Dim res As Integer
    If Not IsNothing(m_LastTarget) Then
        res = m_LastTarget.DragDrop(pDataObj, _
                        grfKeyState, pt, pdwEffect)
        If res <> 0 Then
            Debug.WriteLine("Error in dropping on " + 
                    "DropTarget. res = " & Hex(res))
        Else  'No error on drop
            ' it is quite possible that the 
            ' actual Drop has not completed.
            ' in fact it could be Canceled 
            ' with nothing happening.
            ' All we are going to do is hope for the best
            ' The documented norm for Optimized 
            ' Moves is pdwEffect=None, so leave it
            RaiseEvent ShDragDrop(m_DropList, _
                  m_LastNode, grfKeyState, pdwEffect)
        End If
    End If
    ResetPrevTarget()
    'get rid of cnt added in DragEnter
    Dim cnt As Integer = Marshal.Release(m_DragDataObj)  
    m_DragDataObj = IntPtr.Zero
    Return 0
End Function

All we really do is pass the call on to the IDropTarget interface of the folder that is receiving the drop. That folder takes care of all the details, such as dealing with file overwrites and various user interactions.

However, our work is not done, and may not be doable. The clue is found in the comments in the code. If the drag operation is a copy or "Create Shortcut(s) Here", the operation is normally complete when the folder's IDropTarget.DragDrop routine returns. In that case we have a legitimate expectation of being able to update the control to reflect the new reality. However, if the drag operation is a Move, the Shell folder will almost always execute an Optimized Move. This means that the Move (Copy followed by Delete of the original, or logical equivalent) will be started in a different thread and will run independent of the thread that the control is running on. In extreme, but common, cases, the Move may be cancelled by the user long after the IDropTarget.DragDrop routine returns. For example: the user initiates a drag move and goes to lunch. Upon return, the user notices a message that the Move will result in overwriting an existent directory. The user realizes that he dragged to the wrong spot and cancels the Move. In the meantime, the IDropTarget.DragDrop routine has returned and all the code in the control dealing with the drag has long since completed.

This problem is not unique to ExpTree. There are many clipboard formats supported by the IDataObject, and a surprising number of them were originally designed to deal with this problem. The best of the lot is the CFSTR_LOGICALPERFORMEDDROPEFFECT ("Logical Performed Drop Effect" in .NET). That format will reliably indicate that a Move was the user's final choice, but will also return before the user has had an opportunity to Cancel certain Move operations.

Given the uncertainty of what the drop actually did, ExpTree assumes that the drop operation is complete and calls RefreshNode (which will call CShITem.RefreshDirectories) for both the drop target, and, if directories were involved, the drop source. If the TreeView.SelectedNode is either the drop target or drag source, it also fakes an AfterSelect event so that other controls that are listening for the ExpTreeNodeSelected event have an opportunity to refresh their GUI. The .NET result of all this (pun intended) is that after a Move, the control and subscribers to its ExpTreeNodeSelected event may not reflect the actual state of the underlying data. Since all copy and "Create Shortcut(s) Here" operations, and at least some Move operations, may be complete in time for this updating, the GUI will be correct in most, but not all, cases.

Room for improvement

A caution! Under very specific circumstance (running under the IDE (Debug Mode), on XP, doing a Move of a large file) the Tree would hang. This hang has been fixed, however, the underlying cause has not. Almost anything a person might do to diagnose the problem, causes it to not occur. The problem has never been observed outside of the IDE. Should the problem occur, I have included a Debug.WriteLine to capture the information. If you experience this, please send that information to me.

The behavior described for Optimized Moves is actually handled about as well as possible for .NET IDataObjects. The .NET DataObject seems to interact with the "Logical Performed Drop Effect" behind the scenes and gives a pretty good shot at providing correct information as far as it can. If the drag came from Win Explorer, my class could interact with the CFSTR_LOGICALPERFORMEDDROPEFFECT format to emulate what the .NET DataObject does, giving a better update. Since I anticipate that most drags will actually originate within the same application as the control, I have chosen not to do this at this time.

The only way that I can think of to allow non-FileSystem items to actually be copied or moved via drag ("Create Shortcut(s) Here" actually works) is to obtain a fully working IDataObject from the folder that contains the dragged item. I have tried this approach, without success. I may try it again in the future, but not without some more knowledge, which could be provided by readers of this article (hint,hint).

Credits

I have spent an immense time on the Web researching this work. I am sure that some of the code and quite a few ideas came from people whose name and site I did not note adequately enough to find again for these credits. However, the major helpers were (in no particular order).

  • Dave Anderson who provided a C# version of MakeShellIDArray.
  • Cory Smith for TreeView colorizing technique and code.
  • James Brown at catch22 whose tutorials would have helped more if I had found them earlier.

History

  • 04/20/2012 - Version 2.12.1 update of downloads. Added Windows Explorer style sorting of File/Folder names. xx11xx now sorts before xx101xx. Same downloads as part 1 of this article.
  • 04/19/2012 - Version 2.12 update. This is the same download packages as found in part 1 of this set of articles. The update here is to ensure that the two articles include the same downloads. See that article for a description of changes from the previous version. No changes between the two versions affect the Drag & Drop features described in this article.
  • 03/11/2006 - Update to include the VS2005 version in the download. Various small fixes. Removed Application.DoEvents from CShItem.GetContents. See Readme file for VS2002/VS2005 information.
  • 09/16/2005 - Original release of this article, including Version 2.1 of ExpTreeLib.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here