Opening files via IDropTarget in .Net

A while ago, Raymond Chen wrote about the mechanism for using IDropTarget to receive a list of files to open.

The DropTarget method provides you with an alternative way to register a verb for a file type (i.e. a right-click context menu option). The other alternatives are to receive the files via the command line or via DDE. Since DDE is deprecated, the DropTarget method is specifically meant to replace that.

The primary reason you might want to use this method is if you want to open multiple files in the same application instance. If you select multiple files at once in Explorer and then right-click them and select your custom option, if you used the traditional command line method, your application would be launched multiple times. With the DropTarget method, the application is launched only once and that one instance receives all the files. What’s more, if your application is already running the existing instance will receive the files.

Raymond provided sample code for how to use this method in C++, but what if you want to do this in .Net? Fortunately, it’s relatively easy to do.

Although both Windows Forms and Windows Presentation Foundation have support for drag and drop and probably implement IDragDrop somewhere internally, we need to have access to an implementation we can expose as a COM local server so we cannot use this. And although Windows Forms exposes an IDropTarget interface, this interface does not match the COM interface we need so it’s also of no use to us.

So the first thing we need is the COM IDropTarget interface. I couldn’t find any type library we could import in .Net, so we’ll define the interface manually. Fortunately, it’s not very big so this is pretty easy.

using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00000122-0000-0000-C000-000000000046")] // This is the value of IID_IDropTarget from the Platform SDK.
[ComImport]
interface IDropTarget
{
    void DragEnter([In] IDataObject dataObject, [In] uint keyState, [In] Point pt, [In, Out] ref uint effect);
    void DragOver([In] uint keyState, [In] Point pt, [In, Out] ref uint effect);
    void DragLeave();
    void Drop([In] IDataObject dataObject, [In] uint keyState, [In] Point pt, [In, Out] ref uint effect);
}

Since this code will only ever be used for the shell DropTarget method, and not for actual dragging and dropping, I’ve not bothered to use proper enumerations for keyState and effect, because we won’t use those arguments. Also note I’m making use of the IDataObject COM interface from the System.Runtime.InteropServices.ComTypes namespace.

Besides the COM interface, we’ll also need two define two PInvoke methods as well as a constant for the data format we’ll use.

using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

static class NativeMethods
{
    public const int CF_HDROP = 15;

    [DllImport("shell32.dll", CharSet=CharSet.Unicode)]
    public static extern int DragQueryFile(HandleRef hDrop, int iFile, [Out] StringBuilder lpszFile, int cch);

    [DllImport("ole32.dll")]
    internal static extern void ReleaseStgMedium(ref STGMEDIUM medium);
}

Here we’re using the STGMEDIUM structure, also defined in the System.Runtime.InteropServices.ComTypes namespace.

Next, we have to create a class that implements this interface. The only members the shell ever calls are DragEnter and Drop, and the only one we’ll actually need is Drop, so we’ll leave the rest empty.

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;

[ComVisible(true)]
[Guid("your-guid-here")]
public class MyDropTarget : IDropTarget
{
    public void Drop(IDataObject dataObject, uint keyState, System.Drawing.Point pt, ref uint effect)
    {
        FORMATETC format = new FORMATETC()
        {
            cfFormat = NativeMethods.CF_HDROP,
            dwAspect = DVASPECT.DVASPECT_CONTENT,
            tymed = TYMED.TYMED_HGLOBAL
        };
        STGMEDIUM medium;
        string[] files;
        dataObject.GetData(ref format, out medium);
        try
        {
            IntPtr dropHandle = medium.unionmember;
            int fileCount = NativeMethods.DragQueryFile(new HandleRef(this, dropHandle), -1, null, 0);
            files = new string[fileCount];
            for( int x = 0; x < fileCount; ++x )
            {
                int size = NativeMethods.DragQueryFile(new HandleRef(this, dropHandle), x, null, 0);
                if( size > 0 )
                {
                    StringBuilder fileName = new StringBuilder(size + 1);
                    if( NativeMethods.DragQueryFile(new HandleRef(this, dropHandle), x, fileName, fileName.Capacity) > 0 )
                        files[x] = fileName.ToString();
                }
            }
        }
        finally
        {
            NativeMethods.ReleaseStgMedium(ref medium);
        }

        // Do something with the files here.
    }

    public void DragEnter(System.Runtime.InteropServices.ComTypes.IDataObject dataObject, uint keyState, System.Drawing.Point pt, ref uint effect)
    {
    }

    public void DragOver(uint keyState, System.Drawing.Point pt, ref uint effect)
    {
    }

    public void DragLeave()
    {
    }
}

This code simply retrieves the files we were passed and stores them in an array. It’s up to you to do something interesting with them afterwards (and I’d recommend not doing any further processing on the thread that called Drop because you’ll make the shell wait for your drop handler to complete).

Finally, you need to put some special code in your Main method to make the COM server available to other applications while your application is running.

static void Main(string[] args)
{
    RegistrationServices reg = new RegistrationServices();
    // this is the equivalent to CoRegisterClassObject in Win32
    int cookie = reg.RegisterTypeForComClients(typeof(MyDropTarget), RegistrationClassContext.LocalServer, RegistrationConnectionType.MultipleUse);
    try
    {
        if( args.Length > 0 &&
            !(string.Equals(args[0], "-embedding", StringComparison.OrdinalIgnoreCase) ||
              string.Equals(args[0], "/embedding", StringComparison.OrdinalIgnoreCase)) )
        {
            // If your application was launched by COM as a local server, it'll specify the -Embedding or /Embedding switch.
            // If the -embedding argument was not specified, we should process the command line normally
            OpenFilesFromCommandLine(args);
        }

        // Run your application, e.g. by using Application.Run in Windows Forms.
    }
    finally
    {
        reg.UnregisterTypeForComClients(cookie);
    }
}

Now all that’s left to do is to create the relevant registry entries, which Raymond already covered and which is not any different for .Net:

[HKCR\CLSID\{your-guid-here}\LocalServer32]
@="C:\\Path\\To\\Your\\.Net\\App.exe"

[HKCR\txtfile\shell\yourverb\DropTarget]
"Clsid"="{your-guid-here}"

This registers your application as a context-menu item for text files. Change txtfile to the appropriate value for the file type you wish to handle. Note that you can also register per-user by putting these keys in HKCU\Software\Classes rather than HKCR.

Note that you shouldn’t register your assembly using regasm.exe. Regasm.exe can only be used to register in-process COM servers, not local servers. It also registers your object via the .Net COM InterOp proxy DLL which isn’t necessary for out-of-process COM.

I’ve created a small sample that shows you how to use these techniques in a GUI application: download the ShellDropTargetSample here.

That sample is a .Net 4.0 WPF application created in Visual Studio 2010, but the techniques discussed here are also usable with older versions of .Net and with Windows Forms and console applications.

Note that the sample registers the COM object and parses the command line in App.OnStartUp (in the App.xaml.cs file) rather than the Main method.

Categories: Programming
Posted on: 2010-07-03 10:06 UTC.

Comments

AttractiveAnthroAnteater

2010-07-04 00:44 UTC

The fabled Sven returns! Exploring the desolate ruins of Ooki, I thought that he had been a myth, something made up by the lingering archaeologists. I had hunted the abandoned posts, the borders of blue and white, the islands of grey and the rivers of words, hoping for a sign of the mysterious Dutchman. I had begun to lose hope of anything appearing. But the Dutch god of Ooki has graced us with his presence, adding a new island to his grand kingdom.

The archaeologists were taken aback, as was I. He emerged from the ground, surrounded by an intense azure aura. He lifted his palms, absorbing the heat of the earth and the song of the seraphim. And he pointed his mighty finger to the land, and commanded that there be science.

I immediately fetched my journal, to document the impressive sight. I hoped to get a sketch of his flowing hair and his mighty facial hair. But he disappeared before I could bring pencil to paper, and left us all in the wake of his glory.

AttractiveAnthroAnteater

2010-07-04 16:48 UTC

Hey, my comment doesn't show up on the main page. Is it an error, or did you attempt to delete it?

Sven Author comment

2010-07-04 23:55 UTC

Your comment shows up just fine for me.

AttractiveAnthroAnteater

2010-07-05 01:42 UTC

Oh hey, it works now. Maybe my computer was possessed by Lucifer again, as is its wont. You know how when you scroll down past text clippings very quickly, it shows a bit of Latin before the thumbnail loads? That's the DEVIL. Mephistopheles infests our computers. Quia peccavi nimis cogitatone, verbo et opere. Mea culpa. Mea maxima culpa.

Satan is my lord, he writhes through my files and lurks in my mail. She emerged from my System Preferences, and now dominates everything. She distracts me, she compels me to save more furry pictures and she controls my typing hands.

We are all controlled. The Lady of the dead, the Queen of evil, the Goddess of danger and death, of vice and insanity, of love and hate, passion and power.

And I'm fine with that.

John Schroedl

2010-07-16 22:05 UTC

Wow - excellent post! I'm definitely going to incorporate this into my apps. After reading Raymond's post and the advice to use IDropTarget I've had this on my to-do-someday list. Thanks again.

John

Tim

2010-07-21 09:25 UTC

Great post, and a big thanks for the Ookii.Dialogs library, trying to find an implementation of the Vista Folder Browser for .NET was driving me insane, and it's been a long time since I played with Win32.

John Schroedl

2010-10-18 19:56 UTC

You might want to update your sample a bit. It's checking for "-embedded" instead of "-embedding" in OnStartup().

Also, if the app is up and running and registered, the context menu from Explorer works great. But, if the app is registered but not already running and the menu is used in Explorer, the app will launch but no file names will show in the list and Explorer will time-out waiting for it. I'm trying on Win7 (64-bit).

John

Rajendar

2012-02-17 10:00 UTC

Please can you share, above implementation in c++ ?

Pouriya

2012-02-19 14:51 UTC

You are awesome, :)

Stefan

2012-06-20 05:10 UTC

@John, for a .NET app to work as a local COM server it must not be built as "Any CPU" - it should have specific builds for both x64 and x86 and both should be registered in the respective class roots.

For a discussion see http://stackoverflow.com/questions/9303814/com-cannot-start-out-of-process-net-server-compiled-as-anycpu

Medinoc

2013-07-05 06:19 UTC

Thanks for this tutorial. By reading it from the other side, I can do the managed version of Raymond's "simulating a drop", which I was looking for.

Add comment

Comments are closed for this post. Sorry.

Latest posts

Categories

Archive

Syndication

RSS Subscribe