Hi all,

I have an application with a ListView of remote files. I'm trying to implement a drag-drop of these files between my application and e.g. windows explorer. I have an object implementing IDataObject used for the DragDrop operation and the files are passed in a string[].

The problem is that explorer calls GetData() multiple times (e.g. > 20 times) before the drop has occurred. Because the source files are remote, they need to be downloaded so I'd only like to download them after the drop has occurred and not before. How is this done?

Thanks in advance,
Doug

Dani AI

Generated

Short practical summary (for , building on 's hint):

Explorer will probe your IDataObject many times; that is normal. The robust way to avoid downloading remote files up-front is to advertise them as "virtual files" (FILEGROUPDESCRIPTOR / FILEDESCRIPTOR) and supply actual contents only when the target requests CFSTR_FILECONTENTS (usually as a TYMED_ISTREAM). That lets the shell show names/icons without forcing a fetch of every byte until the drop is committed. (learn.microsoft.com) (codeproject.com)

Practical pattern to implement

  • In GetData for the file-descriptor format return only metadata (names, attributes, optional size).
  • For file-contents return an IStream-style object that does not start the download until either (a) the stream is actually read, or (b) you know the drop was confirmed.
  • Hook the source-side QueryContinueDrag event to detect when the drag loop signals "Drop" and set a small boolean/flag your IDataObject can observe. That flag is the signal to start the real download / open the stream; until then the stream stays dormant. (learn.microsoft.com)

Minimal pseudocode (illustrative — do not copy the forum's IDataObject verbatim)

// on drag start
_dropConfirmed = false;
this.QueryContinueDrag += OnQueryContinueDrag;
DoDragDrop(myVirtualDataObject, DragDropEffects.Copy);
this.QueryContinueDrag -= OnQueryContinueDrag;

void OnQueryContinueDrag(object s, QueryContinueDragEventArgs e) {
  if (e.Action == DragAction.Drop) _dropConfirmed = true;
}

// inside VirtualDataObject.GetData("FileContents", index):
// return a stream wrapper that waits until _dropConfirmed (or until first Read)
return new DeferredStream(() => {
  // this callback runs off the UI thread: do the download and return a readable stream
  return DownloadRemoteFileAsStream(remoteUrl);
});

Troubleshooting notes and cautions

  • Do not perform downloads synchronously on the UI thread inside GetData — that can hang DoDragDrop. Make the stream perform IO on a background thread or return a stream that blocks the caller only as needed.
  • Some targets (or remote environments) may not support virtual-file formats; add a fallback that materializes a temp file and supplies a CF_HDROP array when required. Test with Explorer, Outlook and any specific target apps you care about. (codeproject.com)

This approach keeps the UI responsive, preserves the "lazy" behavior you want, and directly addresses the repeated GetData calls by deferring heavy work until the destination legitimately asks for file bytes.

Recommended Answers

All 6 Replies

Can you paste your implementation of your DragDrop event please and any other events you have linked up related to DragDrop operations.

Hi Ketsuekiame, thanks for the reply.

The drag-drop handlers are:

private void lvFiles_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop)) 
        e.Effect = DragDropEffects.Copy;
}

private void lvFiles_DragDrop(object sender, DragEventArgs e)
{
    if (e.Data.GetDataPresent(DataFormats.FileDrop))
    {
        string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
        foreach (string file in files)
        {
            Text = "Copying " + file + " to " + remoteDir + "...";
            // ...
        }
    }
}

private void lvFiles_ItemDrag(object sender, ItemDragEventArgs e)
{
    System.Diagnostics.Debug.Print("ItemDrag");
    MyFileDataObject dfo = new MyFileDataObject();
    dfo.getDataCallback += new Func<object>(dfo_getDataCallback);
    DoDragDrop(dfo, DragDropEffects.Move);

    System.Diagnostics.Debug.Print("{0} = {1}\r\n{2} = {3}", "Button", e.Button, "Item", e.Item);
}

object dfo_getDataCallback()
{            
    System.Diagnostics.Debug.Print("dfo_getDataCallback");
    return new string[] { "c:\\temp\\junk.txt" };
}

private void lvFiles_DragLeave(object sender, EventArgs e)
{
    System.Diagnostics.Debug.Print("DragLeave");
}

private void lvFiles_DragOver(object sender, DragEventArgs e)
{
    System.Diagnostics.Debug.Print("DragOver: " + string.Join(", ", e.Data.GetFormats()));
}

The class implementing IDataObject is thus:

class MyFileDataObject : IDataObject
{
    public event Func<object> getDataCallback;

    // Returns: The data associated with the specified format, or null.
    public object GetData(string format)
    {
        if (format == DataFormats.FileDrop && getDataCallback != null)
            return getDataCallback();

        return null;
    }

    public bool GetDataPresent(string format)
    {
        return format == DataFormats.FileDrop;
    }

    public string[] GetFormats()
    {
        return new string[] { DataFormats.FileDrop };
    }

    public object GetData(Type format){return GetData(format.ToString());}
    public object GetData(string format, bool autoConvert){return GetData(format);}

    public bool GetDataPresent(Type format){return GetDataPresent(format.ToString());}
    public bool GetDataPresent(string format, bool autoConvert){return GetDataPresent(format);}

    public string[] GetFormats(bool autoConvert){return GetFormats();}
    public void SetData(object data){throw new Exception("Unimplemented");}
    public void SetData(string format, object data){SetData(data);}
    public void SetData(Type format, object data){SetData(data);}
    public void SetData(string format, bool autoConvert, object data){SetData(data);}
}

It's a bit complicated to explain in a post but this link will explain how, why and when GetData is called. MSDN Link

I can't really help you any more than that without writing a couple of sides of A4 ;)

commented: Very appropriate link +6

Thanks very much for the link, it looks like it deals with exactly what I'm after for lazy load. I haven't got a chance to try it out yet.

I also found an already implemented virtual file DataObject which can pass the stream, which I will try first
http://blogs.msdn.com/b/delay/archive/2009/10/26/creating-something-from-nothing-developer-friendly-virtual-file-implementation-for-net.aspx

So there's still the problem I was having before of the GetData being called before the drop is complete.. how do you tell when the final drop has happened?

You can call the method QueryContinueDrag this will return one of the following:

S_OK (keep the drag/drop loop going)
DRAGDROP_S_DROP (Do the drop and complete the drag/drop operation)
DRAGDROP_S_CANCEL (Drag/Drop operation was cancelled)

It would probably be appropriate to call this in your GetData method. Once the result becomes DRAGDROP_S_DROP you can actually fill the data object with real data.

I can't think of any other way around this as Explorer isn't under our control ^^

commented: good lead +6

That sounds simple way to go about it! Apparently that function shouldn't be called directly (http://msdn.microsoft.com/en-us/library/windows/desktop/ms690076(v=vs.85).aspx), but no matter.

In System.Windows.Forms, the Control.QueryContrinueDrag is an event that provides me with the mouse button status (and Alt/Ctrl/Shift), so I can just check for a mouse button release (without abortion request) to signal that the drop should go ahead. Alternately, if this happens at the wrong time, I can just check the mouse buttons (and drop abortion) in the GetData callback.

I'll try this tonight. Thanks again for your help.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.