Tbale of contents
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.
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
Directory classes allow for
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
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.
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
TVDragWrapper instance will now receive and handle the
DragDrop events. No, .NET style drag related event handlers are required.
There is a division of labor between
TVDragWrapper deals with the mechanics of the drag/drop and brokering between the
IDataObject being dragged and the
IDropTarget interface of the
ExpTree deals with managing the appearance of the
TreeView as the drag continues. Communication with
ExpTree is done via four events declared in
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
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
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
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
IDataObject interface and reflecting changes in the application display. Each has its own set of details.
The IDropTarget interface
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:
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 = _
grfKeyState, pt, pdwEffect)
If res = 0 Then
res = m_LastTarget.DragOver(grfKeyState, _
If res <> 0 Then
pdwEffect = 0
pdwEffect = 0
RaiseEvent ShDragOver(tn, ptClient, _
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
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
pdweffect which is exactly equivalent to .NET's
The IDataObject interface
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.
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
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.
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
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
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.
IDataObject must have data in one or more of the following formats:
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
CProcDataObject will attempt to create one.
FileDrop format. The last choice, for good reasons, but also the easiest format for .NET applications to make.
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
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)
m_DataObject = pDataObj
Dim HadError As Boolean = False
IDO = Marshal.GetTypedObjectForIUnknown(pDataObj, _
Catch ex As Exception
HadError = True
If HadError Then
NetIDO = Marshal.GetTypedObjectForIUnknown(pDataObj, _
IsNet = True
IsNet = False
If IsNet Then
If HadError Then Exit 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
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
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
MakeDragListFromCIDA which creates an
CShItems from a CIDA represented as a
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
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
Dim i As Integer
For i = 0 To nrItems
offsets(i) = Marshal.ReadInt32(ptr, curB)
curB += 4
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
MakeStreamFromCIDA = New MemoryStream(_
pidlLen + (4 * offsets.Length) + 4)
Dim BW As New BinaryWriter(MakeStreamFromCIDA)
For i = 0 To nrItems
For i = 0 To nrItems
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
Showing the results of the drop
Given the work done by
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 _
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))
RaiseEvent ShDragDrop(m_DropList, _
m_LastNode, grfKeyState, pdwEffect)
Dim cnt As Integer = Marshal.Release(m_DragDataObj)
m_DragDataObj = IntPtr.Zero
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).
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
- Cory Smith for
TreeView colorizing technique and code.
- James Brown at catch22 whose tutorials would have helped more if I had found them earlier.
- 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
CShItem.GetContents. See Readme file for VS2002/VS2005 information.
- 09/16/2005 - Original release of this article, including Version 2.1 of