您的位置:首页 > 运维架构

Creating something from nothing [Developer-friendly virtual file implementation for .NET!]

2013-05-03 11:06 471 查看
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. :( Specifically, the DataObject
class is where we'd expect to find such support, but the closest it has is SetFileDropList.
And while that sounds promising, it's really just a list of strings with paths to existing files. Recall that the DragDrop.DoDragDrop
method is synchronous (i.e., does not return until the drag-and-drop operation is complete), and the obvious consequence is that creating a virtual file on the fly isn't practical with this API. Specifically, the bits already need to exist on disk by the
time the user starts the drag operation - but you don't know what data they're going to drag until they start! It's a classic Catch-22...

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 actualIDataObject 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: The
DoDragDropOrClipboardSetDataObject
method used above is a simple helper method for the test application - it calls
DragDrop.DoDragDrop
or
Clipboard.SetDataObject
depending on what the user did.
It's not very exciting, so I won't be showing it (or the
VirtualFileDataObject
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 alambda 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... :)

Referenced from: http://blogs.msdn.com/b/delay/archive/2009/10/26/creating-something-from-nothing-developer-friendly-virtual-file-implementation-for-net.aspx
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐