Series Links

This is part of a 3 part series:

  1. Shell Style Drag and Drop in .NET (WPF and WinForms)
  2. Shell Style Drag and Drop in .NET - Part 2
  3. Shell Style Drag and Drop in .NET - Part 3
Introduction

In Part 1, Shell Style Drag and Drop in .NET (WPF and WinForms), I opened up the discussion about implementing a nice Shell style drag image, like that of Windows Explorer, in C#. This involved exposing a couple of COM interfaces to .NET, as well as implementing the COM IDataObject interface. In Shell Style Drag and Drop in .NET - Part 2 I took it to the next step, implementing some extension methods and a helper class. These made it easy to use the COM interfaces to achieve Shell integration.

In Part 3, I will open the door even wider. My aim is to achieve complete Shell integration. First, I will look at adding a drop description. This is the little preview text that tells you what you would do by dropping at the current location. It is a dynamic text with an icon, indicating whether you are moving, copying, linking, etc. Then I'll look at handling all the drag source plumbing in a consistent fashion through the use of a new class, the DragSourceHelper. Unlike the DropTargetHelper class from Part 2, the DragSourceHelper actually has a decent amount of work to do.

Background

In Parts 1 and 2 I utilized the IDragSourceHelper COM interface exposed by the Windows Shell. In this part we will take a look at IDragSourceHelper2, which inherits IDragSourceHelper and exposed an additional function, SetFlags, for allowing drop descriptions.

  • IDragSourceHelper2 - Exposes a method that adds functionality to IDragSourceHelper. This method sets the characteristics of a drag-and-drop operation over an IDragSourceHelper object.

The interface, like IDragSourceHelper, is implemented by the system DragDropHelper class, which is CoCreated using CLSID_DragDropHelper.

The Solution

The solution for this post is quite large, because we are bringing everything together for a complete solution. The solution comes in several parts, in each of which we'll address a major requirement for the overall solution.

Finishing the Implementation with Drop Descriptions

To get drop descriptions working, I need to set a flag on the drag image manager. To do this, I use the IDragSourceHelper2 interface:

[ComVisible(true)] [ComImport] [Guid("83E07D0D-0C5F-4163-BF1A-60B274051E40")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IDragSourceHelper2 { void InitializeFromBitmap( [In, MarshalAs(UnmanagedType.Struct)] ref ShDragImage dragImage, [In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject); void InitializeFromWindow( [In] IntPtr hwnd, [In] ref Win32Point pt, [In, MarshalAs(UnmanagedType.Interface)] IDataObject dataObject); void SetFlags( [In] int dwFlags); }

The IDragSourceHelper2 interface provides the SetFlags function. According to MSDN, there is only one flag accepted by this function, and that is DSH_ALLOWDROPDESCRIPTIONTEXT which is defined in shlobjidl.h as 0x1. We will utilize this method solely to enable drop descriptions.

I also need to introduce a native structure, DROPDESCRIPTION, as a managed structure:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Size = 1044)] public struct DropDescription { public int type; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szMessage; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szInsert; }

Note that the size of the szMessage and szInsert fields is limited to 260 Unicode characters during marshaling. When I set the drop description, this is the structure that I need to set onto the IDataObject for the drag image manager to read and render from.

I also need a new enum, which I've introduced separately in the System.Windows and System.Windows.Forms namespaces for ease of use:

public enum DropImageType { Invalid = -1, None = 0, Copy = (int)DragDropEffects.Copy, Move = (int)DragDropEffects.Move, Link = (int)DragDropEffects.Link, Label = 6, Warning = 7 }

Not much to say here. These are the values for the "type" field on the DropDescription structure. Many of the values correlate directly to the DragDropEffects enum.

To set the drop description on the data object, I introduce an extension method to the SWF and WPF IDataObject interfaces. As in previous parts, I will use the SWF implementation in my examples:

public static void SetDropDescription(this IDataObject dataObject, DropImageType type, string format, string insert) { if (format != null && format.Length > 259) throw new ArgumentException("Format string exceeds the maximum allowed length of 259.", "format"); if (insert != null && insert.Length > 259) throw new ArgumentException("Insert string exceeds the maximum allowed length of 259.", "insert"); // Fill the structure DropDescription dd; dd.type = (int)type; dd.szMessage = format; dd.szInsert = insert; ComTypes.ComDataObjectExtensions.SetDropDescription((ComTypes.IDataObject)dataObject, dd); }

Here I accept a DropImageType enum value, a format string and an insert string. See the documentation for DROPDESCRIPTION on MSDN for details about format and insert, but basically know that you can specify a format like "Move to %1" where "%1" gets replaced with the insert string upon rendering. This is to distinguish the verb from the location, for example. The drag image manager renders the parts of the drop description slightly different to achieve the distinction.

This method leaves some of the work to the ComDataObjectExtensions.SetDropDescription method, which is actually an extension method for the COM IDataObject interface. I separated the logic like this because my SWF and WPF implementations can both take advantage of it. Let's look at that method now:

public static void SetDropDescription(this IDataObject dataObject, DropDescription dropDescription) { ComTypes.FORMATETC formatETC; FillFormatETC(DropDescriptionFormat, TYMED.TYMED_HGLOBAL, out formatETC); // We need to set the drop description as an HGLOBAL. // Allocate space ... IntPtr pDD = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(DropDescription))); try { // ... and marshal the data Marshal.StructureToPtr(dropDescription, pDD, false); // The medium wraps the HGLOBAL System.Runtime.InteropServices.ComTypes.STGMEDIUM medium; medium.pUnkForRelease = null; medium.tymed = ComTypes.TYMED.TYMED_HGLOBAL; medium.unionmember = pDD; // Set the data ComTypes.IDataObject dataObjectCOM = (ComTypes.IDataObject)dataObject; dataObjectCOM.SetData(ref formatETC, ref medium, true); } catch { // If we failed, we need to free the HGLOBAL memory Marshal.FreeHGlobal(pDD); throw; } }

In this method I marshal the structure to an HGLOBAL and then set the data on the COM IDataObject instance.

Ok, there is one last thing to do, which is a bit more obscure. For the drag image manager to properly re-render the drag image with changing drop descriptions, I need to invalidate the drag image by sending a Window message to the drag window. The drag manager provides the drag window's HWND in the data format "DragWindow" on my IDataObject, so I can grab that and call the SendMessage Win32 API:

private const uint WM_INVALIDATEDRAGIMAGE = 0x403; public static void InvalidateDragImage(IDataObject dataObject) { if (dataObject.GetDataPresent("DragWindow")) { IntPtr hwnd = GetIntPtrFromData(dataObject.GetData("DragWindow")); PostMessage(hwnd, WM_INVALIDATEDRAGIMAGE, IntPtr.Zero, IntPtr.Zero); } } private static IntPtr GetIntPtrFromData(object data) { byte[] buf = null; if (data is MemoryStream) { buf = new byte[4]; if (4 != ((MemoryStream)data).Read(buf, 0, 4)) throw new ArgumentException("Could not read an IntPtr from the MemoryStream"); } if (data is byte[]) { buf = (byte[])data; if (buf.Length < 4) throw new ArgumentException("Could not read an IntPtr from the byte array"); } if (buf == null) throw new ArgumentException("Could not read an IntPtr from the " + data.GetType().ToString()); int p = (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0]; return new IntPtr(p); }

You won't find WM_INVALIDATEDRAGIMAGE in documentation, its value is actually WM_USER + 3. I don't remember where I found this, but it works. Would be interesting to know if there are other messages that the drag window accepts to get around some of the problems I'll touch on later.

This InvalidateDragImage method should be called by your drag source's GiveFeedback event handler. I'll walk you through a more complete GiveFeedback event handler later when I talk about the DragSourceHelper class.

Drop Descriptions Summary

Well, that's that. With these new foundations, I can enable and set drop descriptions for my drag images in my application. To enable the drop descriptions, I need to make sure to call SetFlags with an argument of 1 before I set the drag image via IDragSourceHelper.InitializeFromBitmap or IDragSourceHelper.InitializeFromWindow. To set the drop description, during DragEnter or DragOver events I can call e.Data.SetDropDescription with my desired arguments. The last thing to remember is that the drag source needs to listen to the GiveFeedback event and invalidate the drag window.

What Happened to the Managed SetData?

In Parts 1 and 2 when I implemented the COM IDataObject and set it as the inner data store for the SWF and WPF DataObjects, I forgot to address one issue. Both the SWF and WPF versions of DataObject implement both the respective IDataObject and the COM IDataObject. The reason I had to implement the COM IDataObject myself is that the .NET DataObjects don't allow setting data by calling the COM IDataObject.SetData method. They throw a COMException with E_NOTIMPL, which is basically a NotImplementedException. That prevented me from being able to use the .NET DataObjects because the Shell DragDropHelper requires the ability to call that COM callable method to store information for the drag image manager. However, the .NET DataObjects do allow you to provide a COM IDataObject to the constructors, and then would allow me to pass COM calls to the wrapped object directly. That is exactly how I implemented my solution.

(Note: I use ".NET SetData" and "COM SetData" to distinguish the SetData methods exposed by the .NET IDataObject and COM IDataObject respectively. The .NET SetData method is the one you, as a .NET developer, usually call. The COM SetData method is the one that is exposed by the COM IDataObject and is COM-callable. Also, where I've used SWF and/or WPF in the past, I will summarize as just ".NET". The two implementations of data objects are very very similar. So, when I say ".NET IDataObject interface(s)" I mean SWF or WPF, they are pretty much interchangable.)

Well, that put a new block on me. Now, the .NET DataObject that wrapped my COM IDataObject doesn't allow a .NET SetData call. It throws an InvalidOperationException. It is actually an internal converter class that throws the exception. The internal converter class implements the .NET IDataObject interface, and receives the .NET SetData call, but does nothing except throw an exception. The converter class gets created when I pass the COM IDataObject to the constructor of the .NET DataObject, which holds onto it as an internal data store, instead of the default internal data store. I drew up a diagram to show where my .NET SetData calls get blocked:

 
COM IDataObject Wrapped by .NET DataObject

Notice that there is a converter data object wrapping the COM IDataObject, and that is where the .NET SetData call fails. All other GetData and SetData calls go through as expected.

So, why not just implement the .NET IDataObject as well? Well, I certainly went down that path. Unfortunately, when you call DoDragDrop, which accepts the .NET IDataObject interface, as opposed to the DataObject class, somewhere along the line it gets wrapped by a .NET DataObject anyway. This wouldn't be a big deal, except there is another problem. The .NET DataObject classes seem to be less robust than I think they were intended. When I pass an instance of a class that implements both the .NET IDataObject interface as well as the COM IDataObject interface to the constructor, the .NET DataObject recognizes it as a .NET IDataObject and references it directly as its internal data store. That's great! Right? Well, no. The COM SetData method of the .NET DataObjects, as you can see in the above diagram, get a direct reference to the COM IDataObject of the internal converter data object class. Well, if the internal data store isn't of that type, then it will throw an exception, the same one I saw that made me implement the COM IDataObject interface to begin with. Here is a diagram showing this situation:


.NET/COM IDataObject Wrapped by .NET DataObject

As you can see, by the .NET DataObject classes not noticing that the internal data store is a COM IDataObject as well as a .NET IDataObject, I can't utilize both versions of SetData. So now what? Well, we only really have one choice. Because we require the COM SetData to function, we need to accept the fact that the .NET SetData won't. At least, not directly. Luckily, I have complete control, as the creator of the DataObject, and so I'll just write a new extension function, SetDataEx, which will accept managed data, marshal it to an HGLOBAL, and set it using the COM SetData. Let's get to it!

I don't need a completely robust solution, because in many cases, I can use the .NET DataObject classes as a converter. But I'll come back to that in a bit. For now, I need a function that takes managed data, marshals it, and sets it to the COM IDataObject using the COM SetData method. This seems like a great candidate for an extension method on the COM IDataObject interface, so that's exactly how I'll implement it. Note, this is not the same as the SetDataEx function I mentioned, which will use this method internally:

public static void SetManagedData(this IDataObject dataObject, string format, object data) { // Initialize the format structure ComTypes.FORMATETC formatETC; FillFormatETC(format, TYMED.TYMED_HGLOBAL, out formatETC); // Serialize/marshal our data into an unmanaged medium ComTypes.STGMEDIUM medium; GetMediumFromObject(data, out medium); try { // Set the data on our data object dataObject.SetData(ref formatETC, ref medium, true); } catch { // On exceptions, release the medium ReleaseStgMedium(ref medium); throw; } }

This method really handles just the conversion of a string format to the FORMATETC structure. The meat of the marshaling is in the GetMediumFromObject function:

private static void GetMediumFromObject(object data, out STGMEDIUM medium) { // We'll serialize to a managed stream temporarily MemoryStream stream = new MemoryStream(); // Write an indentifying stamp, so we can recognize this as custom // marshaled data. stream.Write(ManagedDataStamp.ToByteArray(), 0, Marshal.SizeOf(typeof(Guid))); // Now serialize the data. Note, if the data is not directly serializable, // we'll try type conversion. Also, we serialize the type. That way, // during deserialization, we know which type to convert back to, if // appropriate. BinaryFormatter formatter = new BinaryFormatter(); formatter.Serialize(stream, data.GetType()); formatter.Serialize(stream, GetAsSerializable(data)); // Now copy to an HGLOBAL byte[] bytes = stream.GetBuffer(); IntPtr p = Marshal.AllocHGlobal(bytes.Length); try { Marshal.Copy(bytes, 0, p, bytes.Length); } catch { // Make sure to free the memory on exceptions Marshal.FreeHGlobal(p); throw; } // Now allocate an STGMEDIUM to wrap the HGLOBAL medium.unionmember = p; medium.tymed = ComTypes.TYMED.TYMED_HGLOBAL; medium.pUnkForRelease = null; }

I just create a MemoryStream, which I will serialize my object to, and then write an internally defined GUID to it. This GUID is a stamp for my code to recognize the data by when I go to get the data. I didn't mention it before, but the .NET GetData will only partially work for me. If a type is serializable (implements ISerializable or is marked by the SerializableAttribute) then the .NET GetData method will still work. If not, I'll have to use another new function, GetDataEx to get my data. We'll see that later. After the stamp, I serialize the type, which I'll need during deserialization because the actual data I serialize may be converted. Then, I use a helper method, GetAsSerializable, that will determine if the data's type is serializable, and if not, will try to convert it to a string using a type converter that the type may have declared through the TypeConverterAttribute. I won't show that code here, because it isn't directly relevant to drag and drop. Finally, I allocate an HGLOBAL and fill the STGMEDIUM structure before returning.

The SetDataEx function is an extension method on the .NET IDataObject interfaces:

public static void SetDataEx(this IDataObject dataObject, string format, object data) { DataFormats.Format dataFormat = DataFormats.GetFormat(format); // Initialize the format structure ComTypes.FORMATETC formatETC = new ComTypes.FORMATETC(); formatETC.cfFormat = (short)dataFormat.Id; formatETC.dwAspect = ComTypes.DVASPECT.DVASPECT_CONTENT; formatETC.lindex = -1; formatETC.ptd = IntPtr.Zero; // Try to discover the TYMED from the format and data ComTypes.TYMED tymed = GetCompatibleTymed(format, data); // If a TYMED was found, we can use the system DataObject // to convert our value for us. if (tymed != ComTypes.TYMED.TYMED_NULL) { formatETC.tymed = tymed; // Set data on an empty DataObject instance DataObject conv = new DataObject(); conv.SetData(format, true, data); // Now retrieve the data, using the COM interface. // This will perform a managed to unmanaged conversion for us. ComTypes.STGMEDIUM medium; ((ComTypes.IDataObject)conv).GetData(ref formatETC, out medium); try { // Now set the data on our data object ((ComTypes.IDataObject)dataObject).SetData(ref formatETC, ref medium, true); } catch { // On exceptions, release the medium ReleaseStgMedium(ref medium); throw; } } else { // Since we couldn't determine a TYMED, this data // is likely custom managed data, and won't be used // by unmanaged code, so we'll use our custom marshaling // implemented by our COM IDataObject extensions. ComTypes.ComDataObjectExtensions.SetManagedData((ComTypes.IDataObject)dataObject, format, data); } }

There is a little bit of magic here. You'll see that I call the SetManagedData function at the end of SetDataEx as a last resort only. The rest of the code actually uses a temporary .NET DataObject instance to act as a converter. First I fill a FORMATETC structure for my desired format. Then I attempt to determine the TYMED based on the format name and data. The method GetCompatibleType is uninteresting, and is based on reflection of the .NET DataObject conversion capabilities. ("Compatible" means that it can be converted during a COM GetData call to the .NET DataObject.) It is slightly different for SWF and WPF, but is largely the same. Once I determine a compatible type, I set the data on the temporary .NET DataObject using its .NET SetData method, then call the COM GetData method, which will do marshaling according to the format and type of data. Then, since the marshaling is done, I can just set the data onto the real data object through the COM SetData method. Notice, only if I fail to determine a compatible TYMED do I do my own marshaling using SetManagedData.

Managed SetData Summary

Now that SetDataEx is in place, it can be used like the .NET SetData method. The only catch is that the data must be either serializable, or have an associated TypeConverter that supports conversion to/from a string. The GetData function will still work other than when the data's type is not serializable. In that case, you can call GetDataEx, which I won't show here. It basically confirms the stamp, then deserializes the data's type, then the data, and performs conversion through a TypeConverter if necessary.

The DragSourceHelper

I've discussed a lot about the heart of the problem and have provided a solid foundation to build a complete solution. However, there are some intricacies to proper implementation that I haven't addressed. We need to handle, as a drag source, the cases when the cursor is over a non-drag image enabled drop target, a drop target that doesn't set the drop description, and the Windows Taskbar (which just throws a curve ball for fun). To handle all this, I've implemented the DragSourceHelper, which is a static class that keeps some smart context to eliminate much of the repetitive client coding and to help you manage drag and drop with less hassle.

Default QueryContinueDrag Handler

I'll be honest up front. The QueryContinueDrag event seems pointless to me. I don't know when a drag source should ever have control over whether to continue a drag and drop operation. Having said that, there is one thing I could think of, that the system, for whatever reason, doesn't handle. That is the escape key. If the user presses escape during the drag and drop operation, most drag sources will just cancel the operation (or should, anyway). That is what my default handler does:

public static void DefaultQueryContinueDrag(QueryContinueDragEventArgs e) { if (e.EscapePressed) { e.Action = DragAction.Cancel; } }

Not much more to say here. This method belongs to my DragSourceHelper class and can be called directly from your QueryContinueDrag event handler, or I also expose a QueryContinueDragEventHandler method, so that you can assign it directly as the handler of your QueryContinueDrag event:

public static void DefaultQueryContinueDragHandler(object sender, QueryContinueDragEventArgs e) { DefaultQueryContinueDrag(e); }

There isn't anything more to say here. For my implementation, that is about all there is.

Default GiveFeedback Handler

The GiveFeedback event handler has a lot more to do than the QueryContinueDrag handler. This is because I have several cases I have to cover:

  • Drop targets that don't support drag images

Problem: If I assume all drop targets support drag images and don't make a special case, I'll end up showing them a plain arrow cursor, and the drop action will be completely unknown to the user.

Solution: For drop targets that don't support drag images, the drag image manager provides a piece of data in my data object that let's me know this is the case. The "IsShowingLayered" data format is a boolean value, true to indicate that the drop target is showing the drag image, false to indicate not. If the flag is on, I'll set UseDefaultCursors to false, then explicitly set the arrow cursor as my current mouse cursor. If it is not on, I'll set UseDefaultCursors to true, and I won't set the current cursor explicitly.

  • Drop targets that support drag images but don't set the drop description

Problem: These targets talk to the Shell's IDropTargetHelper implementation, but don't set the drop description. For these targets, we can provide a default drop description instead of the out-of-date default drag and drop cursors.

Solution: I can handle this by taking care to handle all drag and drop events properly. For one, during DragLeave, I can make sure to set my drop description as invalid, by using the DropImageType.Invalid type. That is what Windows Explorer does, too, so we are consistent. Next, through GiveFeedback, I can detect this invalid drop description, then look at the current drop effect, set by the drop target, and set a default description. That part is easy, the only thing you have to really worry about is the fact that drop targets that don't set the drop description won't invalidate them either! That means I need to track when I have a default drop description currently set. Then I can detect changes to the drop effect and change the current drop description. I'll do this with a flag that is associated to the IDataObject. Well, I lied. That isn't the only thing to worry about. You also need to detect when, after you set a default drop description, a drop target sets a drop description. This can be caused by the mouse leaving the drop target that doesn't set the drop description and entering a drop target that does. I detect this by implementing an IAdviseSink and listening to DataChanged events on the DataObject. I'll address that in the next section, Adding Support for AdviseSinks to the DataObject.

  • The Windows Taskbar

Problem: The Taskbar supports drag images, but doesn't set the drop description. The reason it is in a different category is because it is the most prominent place you'll notice that when you drag over it, from Windows Explorer, the no-smoking icon appears (drop effect None) but you won't see drag text. This is actually true for any drop target that supports drag images, but doesn't set a drop description and has a drop effect of None.

Solution: I can implement this by simply not invalidating the drag image after it has been rendered. That is, if I don't invalidate the drag image, then the drop description's icon will not refresh with the text after a pause. In order to implement this properly, I have to invalidate the drag image once after the drop description is set to None, and then not until it changes again.

The handler, after all of these cases are covered, looks like this:

public static void DefaultGiveFeedback(IDataObject data, GiveFeedbackEventArgs e) { // For drop targets that don't set the drop description, we'll // set a default one. Drop targets that do set drop descriptions // should set an invalid drop description during DragLeave. bool setDefaultDropDesc = false; bool isDefaultDropDesc = IsDropDescriptionDefault(data); DropImageType currentType = DropImageType.Invalid; if (!IsDropDescriptionValid(data) || isDefaultDropDesc) { currentType = GetDropImageType(data); setDefaultDropDesc = true; } if (IsShowingLayered(data)) { // The default drag source implementation uses drop descriptions, // so we won't use default cursors. e.UseDefaultCursors = false; Cursor.Current = Cursors.Arrow; } else e.UseDefaultCursors = true; // We need to invalidate the drag image to refresh the drop description. // This is tricky to implement correctly, but we try to mimic the Windows // Explorer behavior. We internally use a flag to tell us to invalidate // the drag image, so if that is set, we'll invalidate. Otherwise, we // always invalidate if the drop description was set by the drop target, // *or* if the current drop image is not None. So if we set a default // drop description to anything but None, we'll always invalidate. if (InvalidateRequired(data) || !isDefaultDropDesc || currentType != DropImageType.None) { InvalidateDragImage(data); // The invalidate required flag only lasts for one invalidation SetInvalidateRequired(data, false); } // If the drop description is currently invalid, or if it is a default // drop description already, we should check about re-setting it. if (setDefaultDropDesc) { // Only change if the effect changed if ((DropImageType)e.Effect != currentType) { if (e.Effect == DragDropEffects.Copy) data.SetDropDescription(DropImageType.Copy, "Copy", ""); else if (e.Effect == DragDropEffects.Link) data.SetDropDescription(DropImageType.Link, "Link", ""); else if (e.Effect == DragDropEffects.Move) data.SetDropDescription(DropImageType.Move, "Move", ""); else if (e.Effect == DragDropEffects.None) data.SetDropDescription(DropImageType.None, null, null); SetDropDescriptionIsDefault(data, true); // We can't invalidate now, because the drag image manager won't // pick it up... so we set this flag to invalidate on the next // GiveFeedback event. SetInvalidateRequired(data, true); } } }

You'll notice that I first detect if I will need to set the default description, and get the current drop image type. I don't set the default drop description at this point, because it doesn't make a difference. Calling InvalidateDragImage after setting the drop description here will not truly invalidate the drag image. Next, I determine if the drop target supports drag images via my IsShowingLayered helper function. If so, we won't use default cursors, else we will. Next, we determine if we should invalidate the drag image. Basically, if our flag is set from a previous call, or if the drop description was manually set by the drop target, or the drop description's drop image type is not None, then we'll invalidate, plus clear the flag to invalidate next time. Notice, that only when the None effect is applied do we possibly not invalidate the drag image.

Adding Support for AdviseSinks to the DataObject

In order for the GiveFeedback handler described above to work as expected, we have to detect when a drop target manually sets a drop description. There are a couple of ways to do this, but the most reliable is to listen to DataChanged events from the IDataObject. The COM IDataObject has a couple of methods that, until now, have been left as unsupported. They are DAdvise and DUnadvise. They are simply methods that enable you to pass an instance of IAdviseSink to listen for data events. The IAdviseSink interface covers advisory tasks for several system components, but for drag and drop, we just need to implement the OnDataChanged method:

private class AdviseSink : ComTypes.IAdviseSink { // The associated data object private IDataObject data; /// <summary> /// Creates an AdviseSink associated to the specified data object. /// </summary> /// <param name="data">The data object.</param> public AdviseSink(IDataObject data) { this.data = data; } /// <summary> /// Handles DataChanged events from a COM IDataObject. /// </summary> /// <param name="format">The data format that had a change.</param> /// <param name="stgmedium">The data value.</param> public void OnDataChange(ref ComTypes.FORMATETC format, ref ComTypes.STGMEDIUM stgmedium) { // We listen to DropDescription changes, so that we can unset the IsDefault // drop description flag. object odd = ComTypes.ComDataObjectExtensions.GetDropDescription((ComTypes.IDataObject)data); if (odd != null) DragSourceHelper.SetDropDescriptionIsDefault(data, false); } #region Unsupported callbacks public void OnClose() { throw new NotImplementedException(); } public void OnRename(System.Runtime.InteropServices.ComTypes.IMoniker moniker) { throw new NotImplementedException(); } public void OnSave() { throw new NotImplementedException(); } public void OnViewChange(int aspect, int index) { throw new NotImplementedException(); } #endregion // Unsupported callbacks }

Because we only care about DropDescription changes, we will connect an instance of the AdviseSink class for notifications of that format only. The OnDataChange function is the callback from the IDataObject and simply verifies the drop description is accessible and then unsets the IsDefault flag for the data object. This tells the DragSourceHelper that this data object no longer has the default drop description.

The drop description flags I've mentioned, IsDefault and InvalidateRequired, are just a couple of internal flags, which I store in a Dictionary keyed on the IDataObject itself. That way I don't need to set internal-only data on the data object.

The Do-It-All Functions

The DragSourceHelper class isn't too complex, but it is more than I want to go into here. Most of it is just convenient do-it-all or do-it-in-less-lines-of-code functions. Having said that, there are a couple of functions I want to point out. You'll see in the examples, I use the DragSourceHelper.DoDragDrop function. The DoDragDrop function is a one-liner to call for the drag source, and handles all the internal drag and drop logic from the drag source side. The only thing you need to do is prepare a drag image, call DoDragDrop, and then handle the drag drop effect. Of course, for the cases that you need a little more granularity, there are several available functions and overrides to help you get what you need to get done without the hassle. Let's take a look at the DoDragDrop function (I included source for a couple of the called functions for reference):

public static DragDropEffects DoDragDrop( Control control, System.Drawing.Point cursorOffset, DragDropEffects allowedEffects, params KeyValuePair<string, object>[] data) { IDataObject dataObject = RegisterDefaultDragSource(control, cursorOffset); return DoDragDropInternal(control, dataObject, allowedEffects, data); } public static IDataObject RegisterDefaultDragSource( Control control, System.Drawing.Point cursorOffset) { IDataObject data = CreateDataObject(control, cursorOffset); RegisterDefaultDragSource(control, data); return data; } public static void RegisterDefaultDragSource(Control control, IDataObject data) { // Cache the drag source and the associated data object DragSourceEntry entry = new DragSourceEntry(data); if (!s_dataContext.ContainsKey(control)) s_dataContext.Add(control, entry); else s_dataContext[control] = entry; // We need to listen for drop description changes. If a drop target // changes the drop description, we shouldn't provide a default one. entry.adviseConnection = ComTypes.ComDataObjectExtensions.Advise( (ComTypes.IDataObject)data, new AdviseSink(data), DropDescriptionFormat, 0); // Hook up the default drag source event handlers control.GiveFeedback += new GiveFeedbackEventHandler(DefaultGiveFeedbackHandler); control.QueryContinueDrag += new QueryContinueDragEventHandler(DefaultQueryContinueDragHandler); } private static DragDropEffects DoDragDropInternal( Control control, IDataObject dataObject, DragDropEffects allowedEffects, KeyValuePair<string, object>[] data) { // Set the data onto the data object. if (data != null) { foreach (KeyValuePair<string, object> dataPair in data) dataObject.SetDataEx(dataPair.Key, dataPair.Value); } try { return control.DoDragDrop(dataObject, allowedEffects); } finally { UnregisterDefaultDragSource(control); } }

What you'll see is that the DoDragDrop creates an instance of the DataObject, including registering it internally for default GiveFeedback and QueryContinueDrag event handlers and DataChange handler on the DropDescription format. It then adds any data you passed in, and calls the .NET DoDragDop, and then unregisters the event handlers before it returns the result drag drop effect.

This reduces the client code to a one-liner, as in the examples:

DragSourceHelper.DoDragDrop(button1, new Point(e.X, e.Y), DragDropEffects.Link | DragDropEffects.Copy, new KeyValuePair<string, object>(DataFormats.Text, "Hello Drag and Drop!"), new KeyValuePair<string, object>(DataFormats.Html, "<h4>Hello Drag and Drop!</h4>"));

In this example, you may have noticed, we are initializing the drag image from the button control itself, instead of a custom bitmap. Other overrides exist to help you achieve your most intriguing drag and drop experience, yet.

Conclusion

The last couple of weeks have gone deep into the implementation of a complete Shell-integrated drag and drop experience for your .NET apps. Whether you are working in WinForms or WPF, you can provide great user feedback during drag and drop operations. With WPF, this provides a great base for applications that are highly dynamic in the user experience. In WinForms, you can get that true integration that you've felt like .NET lacked.

I encourage you to use the source provided, although please keep in mind it is Microsoft copyrighted.

Source Code

All source for this article is provided below.

File Description
Complete drag and drop library as single CS file. Drop directly into your existing projects. Includes SWF and WPF implementations. No samples.
Complete set of projects for the drag and drop library. Includes samples.
The above link has an important bug fix, but if you need the original sources, you can download this one.