It’s Such a Drag

So, in my day job as a GIS developer, I’ve been making an addin for ArcGIS. Nothing fancy, just a data loader to make it easier for our users to pick out the exact item they want from the many datasets we provide. Choose a few categories, select some items from the list, click the load button, done! But what if you want to just drag the items onto the map? ArcObjects doesn’t appear to offer drag and drop straight out of the box, but it is possible. If you’re also developing ArcGIS addins, I’ll save you some time and explain how it is done.

Implementing drag and drop in .NET is actually pretty straightforward. There are two parts to the transaction – the drag source (the control you are dragging from) and the drop target (the control you are dragging to). All you have to do is add an ItemDrag event to your source control (which calls DoDragDrop to start the drag action), then enable the AllowDrop property on your target control, and add the DragDrop event to handle the dropped item. There are some other events that can be handled, including DragEnter, DragLeave, DragOver, GiveFeedback, and QueryContinueDrag, but that is the basic procedure.

When developing addins for ArcGIS, the drop target is already handled for you – the main window in ArcMap is listening for drop events, such as dropping mxds or datasets from the Catalog window or from ArcCatalog. All you have to do is package your drag source into the correct format and you’re good to go. It couldn’t be easier, right?

Well, it all depends on being able to format the dragged item correctly. As this post from The Sandpit states, ArcMap accepts two kinds of input. When dragging from ArcCatalog, an ESRI Names object is created, which is an enumerator that provides the names of the datasets to be imported. Dragging from ArcMap on the other hand, creates an ESRI Layers object, which is a collection of layers and tables. Apparently setting the drag source to an ESRI Layers object is not supported, and in my addin we are just loading datasets from the database by name, so it looks like I need to use ESRI Names.

Incidentally, if you’re doing any development with clipboard objects, I found two helpful applications. Clipboard Viewer shows whatever is currently on the clipboard and DataObjectViewer shows whatever has just been dragged onto it. You can use either of these to drag a selection of layers or datasets from ArcMap or ArcCatalog to confirm which object type is being used.

So, the example code on the Sandpit post went some way towards constructing an ESRI Names object and setting it as the drag source, but it relied on a third-party dll which does not work on recent versions of ArcGIS. However, according to this Geonet post, you can instead use a MemoryStream to pass the IEnumName to a DataObject, then use the DataObject as the drag source.

I did this, and it worked! The dataset name dropped onto the map and was loaded as a layer. However, the debugger showed a log of exceptions with the message, “A first chance exception of type ‘System.Runtime.InteropServices.COMException’ occurred in System.Windows.Forms.dll”. This is not ideal – I always try to get my code to run as cleanly as possible – so I took a closer look at the DataObjectViewer page above, as it goes into a lot of detail about DataObjects and how to construct one from scratch. I managed to do this, but it didn’t make any difference unfortunately. The consensus among developers on Stack Overflow seems to be that first chance exceptions can be ignored as users don’t see them. Hey ho, as long as it works, right?

The full code is shown below. lvDatasets is the ListView that contains the dataset names to be dragged. To keep this example simple, it has been set up so all the items added to it are IDatasetNames. It may be more useful though, to create a custom class to hold the IDatasetName, the alias, and other related information.

    using System;
    using System.IO;
    using System.Windows.Forms;
    using ESRI.ArcGIS.Carto;
    using ESRI.ArcGIS.esriSystem;
    using ESRI.ArcGIS.Geodatabase;
    using DataObject = System.Windows.Forms.DataObject;

    private void LoadSelectedLayers()
        // declare name objects to method scope and populate if dragging
        INameFactory nameFactory = null;
        IEnumName enumName = null;
        IEnumNameEdit enumNameEdit = null;
        DragDropEffects dropStatus = DragDropEffects.All;
        nameFactory = new NameFactoryClass();
        enumName = new NamesEnumeratorClass();
        enumNameEdit = (IEnumNameEdit)enumName;

        // iterate backwards over selected items so they are added in reverse order (so TOC order is same as list view order)
        for (int i = lvDatasets.SelectedItems.Count - 1; i >= 0; i--)
            IDatasetName datasetName = (IDatasetName)lvDatasets.SelectedItems[i];
            IName tempName = (IName)datasetName;

        // Create a new data object.
        DataObject myDataObject = new DataObject();

        // Add the names to the DataObject.
        MemoryStream memoryStream = new MemoryStream((byte[])nameFactory.PackageNames(enumName));
        myDataObject.SetData("ESRI Names", memoryStream);

        // set the data as a drop source
        dropStatus = lvDatasets.DoDragDrop(myDataObject, DragDropEffects.All);

        if (dropStatus != DragDropEffects.None)
            // reset list view