Creating something from nothing [Developer-friendly virtual file implementation for .NET!]
Have you ever used one of those programs that lets you drag a UI widget, drop it in a folder, and - poof - a file that didn't exist magically appears? Me, too - it's cool! But how does it work? Are they really deferring the work of creating that file until it's needed and then creating the file during the drag-and-drop operation? Yes they are - and now you can, too!
If I have seen a little further it is by standing on the shoulders of Giants.- Sir Isaac Newton
Everything you ever needed to know about drag-and-drop in Windows can probably be found in the MSDN documentation for Transferring Shell Objects with Drag-and-Drop and the Clipboard. That documentation is a great resource for specific questions, but because it covers so many topics, it's not necessarily the best way to get an overview. For that, we turn to Raymond Chen's blog - specifically, a series he did called "What a drag" in March of last year. Raymond's example uses native code exclusively, but don't let that scare you away - his presentation and explanations are always engaging! Please take a moment to read (or at least skim) the following articles, or else the rest of this post might not make much sense:
- What a drag: Dragging text
- What a drag: Dragging a Uniform Resource Locator (URL)
- What a drag: Dragging a Uniform Resource Locator (URL) and text
- What a drag: Dragging a virtual file (HGLOBAL edition)
- What a drag: Dragging a virtual file (IStream edition)
- What a drag: Dragging a virtual file (IStorage edition)
- You can drag multiple virtual objects, you know
Okay, so we know what we want to do and now we know how it's supposed to work! The first thing to consider is whether the WPF platform supports the virtual file scenario. And unfortunately, it doesn't seem to.
The natural next step is to consider whether subclassing DataObject
would help - but it's sealed
, so that's a pretty quick dead end.
Move on to consider whether the System.Windows.IDataObject interface used by DataObject
would be useful. But it seems not; it's pretty much the same API as DataObject
which we've already dismissed.
So that leaves us looking at the System.Runtime.InteropServices.ComTypes.IDataObject interface which is a simple managed representation of the actual IDataObject COM interface that the shell uses directly. Clearly, anything is possible at this point, so if we can just channel our inner Raymond, we ought to be in business!
The good news is that I've already done this for you. I've even written a simple WPF application to show how everything fits together:
[Click here to download the complete code for VirtualFileDataObject and the sample application.]
What I've done is write a custom IDataObject
class called VirtualFileDataObject
that does all the hard work for you. All you need to do is provide the relevant data, and your users will be dragging-and-dropping virtual files in no time. And what's really neat is that writing the code to support drag-and-drop automatically gives complete support for the clipboard because the Clipboard.SetDataObject method uses the same IDataObject
interface!
Let's look at the sample scenarios to understand how VirtualFileDataObject
is used:
Text only
var virtualFileDataObject = new VirtualFileDataObject(); // Provide simple text (in the form of a NULL-terminated ANSI string) virtualFileDataObject.SetData( (short)(DataFormats.GetDataFormat(DataFormats.Text).Id), Encoding.Default.GetBytes("This is some sample text\0")); DoDragDropOrClipboardSetDataObject(e.ChangedButton, Text, virtualFileDataObject);
This is the simplest possible scenario - just to show off that simple things stay simple with VirtualFileDataObject
. Here's the signature for the SetData
method used above:
/// <summary> /// Provides data for the specified data format (HGLOBAL). /// </summary> /// <param name="dataFormat">Data format.</param> /// <param name="data">Sequence of data.</param> public void SetData(short dataFormat, IEnumerable<byte> data)
Note that it operates in "HGLOBAL
mode" where all the data is provided at the time of the call. Note also that it doesn't know anything about what the data is, so it's up to the caller to make sure it's in the right format. Specifically, the right format for DataFormats.Text
is a NULL-terminated ANSI string, so that's what the sample passes in.
Aside: TheDoDragDropOrClipboardSetDataObject
method used above is a simple helper method for the test application - it callsDragDrop.DoDragDrop
orClipboard.SetDataObject
depending on what the user did. It's not very exciting, so I won't be showing it (or theVirtualFileDataObject
constructor) in the following examples.
Text and URL
// Provide simple text and a URL in priority order // (both in the form of a NULL-terminated ANSI string) virtualFileDataObject.SetData( (short)(DataFormats.GetDataFormat(CFSTR_INETURLA).Id), Encoding.Default.GetBytes("http://blogs.msdn.com/delay/\0")); virtualFileDataObject.SetData( (short)(DataFormats.GetDataFormat(DataFormats.Text).Id), Encoding.Default.GetBytes("http://blogs.msdn.com/delay/\0"));
Another simple example based on Raymond's discussion of drag-and-drop into Internet Explorer. For our purposes, this example demonstrates that you can set multiple data formats and that formats other than those exposed by WPF's DataFormats
enumeration are easy to deal with. Per the guidelines, supported formats are provided in order by priority, with higher priority formats coming first.
Virtual file
// Provide a virtual file (generated on demand) containing the letters 'a'-'z' virtualFileDataObject.SetData(new VirtualFileDataObject.FileDescriptor[] { new VirtualFileDataObject.FileDescriptor { Name = "Alphabet.txt", Length = 26, ChangeTimeUtc = DateTime.Now.AddDays(-1), StreamContents = stream => { var contents = Enumerable.Range('a', 26).Select(i => (byte)i).ToArray(); stream.Write(contents, 0, contents.Length); } }, });
At last, something juicy! This example creates a virtual file named Alphabet.txt
that's 26 bytes long and appears to have been written exactly one day ago. The contents of this file aren't generated until they're actually required by the drop target, so there's no wasted effort if the user doesn't start the drag, aborts it, or whatever. When the file's contents are eventually needed, VirtualFileDataObject
calls the user-provided Action
(not necessarily a lambda expression, though I've used one here for conciseness) and passes it a write-only Stream instance for writing the data. The user code writes to this stream as much or as little as necessary, then returns control to VirtualFileDataObject
in order to complete the operation.
The file that gets created when you drop/paste this item into a folder looks just like you'd expect. And because VirtualFileDataObject
supports the length and change time fields, Windows has all the information it needs to help the user resolve possible conflicts:
Here's the relevant SetData
method (note that you can provide an arbitrary number of FileDescriptor
instances, so you can create as many virtual files as you want):
/// <summary> /// Provides data for the specified data format (FILEGROUPDESCRIPTOR/FILEDESCRIPTOR) /// </summary> /// <param name="fileDescriptors">Collection of virtual files.</param> public void SetData(IEnumerable<FileDescriptor> fileDescriptors)
It makes use of another SetData
method that's handy for dealing with "ISTREAM
mode":
/// <summary> /// Provides data for the specified data format and index (ISTREAM). /// </summary> /// <param name="dataFormat">Data format.</param> /// <param name="index">Index of data.</param> /// <param name="streamData">Action generating the data.</param> /// <remarks> /// Uses Stream instead of IEnumerable(T) because Stream is more likely /// to be natural for the expected scenarios. /// </remarks> public void SetData(short dataFormat, int index, Action<Stream> streamData)
And accepts data in the following form:
/// <summary> /// Class representing a virtual file for use by drag/drop or the clipboard. /// </summary> public class FileDescriptor { /// <summary> /// Gets or sets the name of the file. /// </summary> public string Name { get; set; } /// <summary> /// Gets or sets the (optional) length of the file. /// </summary> public UInt64? Length { get; set; } /// <summary> /// Gets or sets the (optional) change time of the file. /// </summary> public DateTime? ChangeTimeUtc { get; set; } /// <summary> /// Gets or sets an Action that returns the contents of the file. /// </summary> public Action<Stream> StreamContents { get; set; } }
Text, URL, and a virtual file!
// Provide a virtual file (downloaded on demand), its URL, and descriptive text virtualFileDataObject.SetData(new VirtualFileDataObject.FileDescriptor[] { new VirtualFileDataObject.FileDescriptor { Name = "DelaysBlog.xml", StreamContents = stream => { using(var webClient = new WebClient()) { var data = webClient.DownloadData("http://blogs.msdn.com/delay/rss.xml"); stream.Write(data, 0, data.Length); } } }, }); virtualFileDataObject.SetData( (short)(DataFormats.GetDataFormat(CFSTR_INETURLA).Id), Encoding.Default.GetBytes("http://blogs.msdn.com/delay/rss.xml\0")); virtualFileDataObject.SetData( (short)(DataFormats.GetDataFormat(DataFormats.Text).Id), Encoding.Default.GetBytes("[The RSS feed for Delay's Blog]\0"));
Finally, here's a sample that pulls everything together in a nice, fancy package with a bow on top. The text is an informative snippet, the URL is a link to an RSS feed, and the virtual file is the dynamically downloaded content of that RSS feed! Way cool - it's like there's this big file sitting around that the user can drop anywhere they want - except that it only really exists on the web and it's always up to date whenever you drop it somewhere!
As you can see, the VirtualFileDataObject
class makes the whole scenario really easy and approachable - even if you're not an expert on shell interoperability. It's pretty snazzy, I'd say.
There's just one small problem...
If you tried the drag-and-drop version of the last sample above on a machine with a slow network connection, you probably noticed that the sample application became unresponsive as soon as you dropped the virtual file and didn't recover until the download completed. This is a natural consequence of the DoDragDrop
method being synchronous and getting called from the UI thread (like it should be). In most scenarios, you probably won't notice this problem because generating the file's data is practically instantaneous. But when there's a delay, unresponsiveness is a possibility. The good news is that there's an official technique for solving this problem. The bad news is that it doesn't work for WPF apps. The good news is that I can show you how to make it work anyway.
But that's a topic for another blog post - one that I'll write in a week or so...
Updated 2017-04-20: The sample behavior is slightly different on Windows 10. The cause was hard to track down, but feedback and investigation have determined that this code and sample work correctly as-is on Windows 10 (just like on previous versions of Windows). What's different is how Windows 10 renders the mouse pointer during a drag of a virtual file when only DragDropEffects.Move
is specified. Although previous versions of Windows would show the "move" icon whether or not the Shift
key was down to force a "move" operation, Windows 10 shows the "no smoking" icon by default and only shows the "move" icon when Shift
is held down. The lack of fallback from "copy" to "move" on Windows 10 makes it seem like the virtual file drag part of the sample is broken, though it works fine when the modifier key is used. The workaround is to use the modifier key or pass DragDropEffects.Copy
.