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:
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 End If
Else
pdwEffect = 0 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) m_DataObject = pDataObj
Dim HadError As Boolean = False Try
IDO = Marshal.GetTypedObjectForIUnknown(pDataObj, _
GetType(ShellDll.IDataObject))
Catch ex As Exception
HadError = True
End Try
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
ProcessNetDataObject(NetIDO)
Else If HadError Then Exit Sub ProcessCOMIDataObject(IDO)
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 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
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 RaiseEvent ShDragDrop(m_DropList, _
m_LastNode, grfKeyState, pdwEffect)
End If
End If
ResetPrevTarget()
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 IDataObject
s. 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
.