Click here to Skip to main content
Click here to Skip to main content

Transferring Virtual Files to Windows Explorer in C#

, 28 Jan 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
An example of transferring virtual files to Windows Explorer using C# and the CFSTR_FILECONTENTS and CFSTR_FILEDESCRIPTOR formats.

Introduction

The firm I work for sells a product that implements its own file server and runtime environment. The code base is old and two years ago a project was initiated to create a new interface using C# and .NET 2.0. One of the features on the wish list was the ability to copy/cut/drag/drop files between the proprietary file server environment and Windows Explorer. Bringing files into the proprietary environment was easy through the use of the CF_HDROP format. However, trying to extract files was proving to be a problem. Since the files do not exist in the Windows environment, the CF_HDROP format was not available. Plus we wanted to use Delayed Rendering so that the files were not extracted from the proprietary environment unless they were absolutely needed in order to cut down on overhead. The best format I was able to find to accomplish what I wanted to do was the CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS formats. Extensive searches of the Internet turned up no examples on doing this in C# and even some comments that it was not even possible in a managed language. After spending many weeks on the problem, I finally came up with the code that I am presenting in this article.

Using the Code

This code implements a class called DataObjectEx that is derived from System.Windows.Forms.DataObject and System.Runtime.InteropServices.ComTypes.IDataObject. Due to the many unique ways that virtual files can be rendered, this code shows what to do with the data once the programmer has the virtual file data to work with. I have left comments at various locations in the code where the virtual file data needs to be supplied to the class.

For this project, the following namespaces need to be referenced:

using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Security.Permissions;

The first class that needs to be defined is a NativeMethods class that will contain various constants and native methods used by the DataObjectEx class:

public class NativeMethods
{
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern IntPtr GlobalAlloc(int uFlags, int dwBytes);
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
    public static extern IntPtr GlobalFree(HandleRef handle);
    // Clipboard formats used for cut/copy/drag operations
    public const string CFSTR_PREFERREDDROPEFFECT = "Preferred DropEffect";
    public const string CFSTR_PERFORMEDDROPEFFECT = "Performed DropEffect";
    public const string CFSTR_FILEDESCRIPTORW = "FileGroupDescriptorW";
    public const string CFSTR_FILECONTENTS = "FileContents";
    // File Descriptor Flags
    public const Int32 FD_CLSID = 0x00000001;
    public const Int32 FD_SIZEPOINT = 0x00000002;
    public const Int32 FD_ATTRIBUTES = 0x00000004;
    public const Int32 FD_CREATETIME = 0x00000008;
    public const Int32 FD_ACCESSTIME = 0x00000010;
    public const Int32 FD_WRITESTIME = 0x00000020;
    public const Int32 FD_FILESIZE = 0x00000040;
    public const Int32 FD_PROGRESSUI = 0x00004000;
    public const Int32 FD_LINKUI = 0x00008000;
    // Global Memory Flags
    public const Int32 GMEM_MOVEABLE = 0x0002;
    public const Int32 GMEM_ZEROINIT = 0x0040;
    public const Int32 GHND = (GMEM_MOVEABLE | GMEM_ZEROINIT);
    public const Int32 GMEM_DDESHARE = 0x2000;
    // IDataObject constants
    public const Int32 DV_E_TYMED = unchecked((Int32)0x80040069);
}

With this class in place, we need to define the namespace and the class DataObjectEx as well as the various structures and class wide variables that will be used:

namespace MyData.Extensions
{
    public class DataObjectEx : 
        DataObject, System.Runtime.InteropServices.ComTypes.IDataObject
    {
        private static readonly TYMED[] ALLOWED_TYMEDS =
            new TYMED[] { 
                TYMED.TYMED_ENHMF,
                TYMED.TYMED_GDI,
                TYMED.TYMED_HGLOBAL,
                TYMED.TYMED_ISTREAM, 
                TYMED.TYMED_MFPICT};
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        struct FILEDESCRIPTOR
        {
            public UInt32 dwFlags;
            public Guid clsid;
            public System.Drawing.Size sizel;
            public System.Drawing.Point pointl;
            public UInt32 dwFileAttributes;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
            public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
            public UInt32 nFileSizeHigh;
            public UInt32 nFileSizeLow;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public String cFileName;
        }
        public struct SelectedItem
        {
            public String FileName;
            public DateTime WriteTime;
            public Int64 FileSize;
        }
        private SelectedItem[] m_SelectedItems;
        private Int32 m_lindex;
        public DataObjectEx(SelectedItem[] selectedItems)
        {
            m_SelectedItems = selectedItems;
        }

The public structure SelectedItem is used to convey various pieces of virtual file information to the class in order to perform delayed rendering as well as provide file name, size and date/time information that is needed for CFSTR_FILEDESCRIPTOR. This structure can be modified as needed by the programmer to provide any additional information that might be needed in order to render the virtual file.

The next method we need to implement is an override of GetData. This override allows us to perform delayed rendering of the CFSTR_FILEDESCRIPTOR and CFSTR_FILECONTENTS formats. When Windows Explorer issues a GetData call, this routine will be called at which time the virtual data will be extracted. In addition, the format CFSTR_PERFORMEDDROPEFFECT is also trapped. This format is called by Windows Explorer when the drop/paste operation is completed. If any cleanup is needed after the transfer, it should be performed here. This format is only requested if the transfer is successfully completed. If the user presses cancel during the transfer, this format will not be requested:

public override object GetData(string format, bool autoConvert)
{
    if (String.Compare(format, NativeMethods.CFSTR_FILEDESCRIPTORW, 
        StringComparison.OrdinalIgnoreCase) == 0 && m_SelectedItems != null)
    {
        base.SetData(NativeMethods.CFSTR_FILEDESCRIPTORW, 
            GetFileDescriptor(m_SelectedItems));
    }
    else if (String.Compare(format, NativeMethods.CFSTR_FILECONTENTS, 
        StringComparison.OrdinalIgnoreCase) == 0)
    {
        base.SetData(NativeMethods.CFSTR_FILECONTENTS, 
            GetFileContents(m_SelectedItems, m_lindex));
    }
    else if (String.Compare(format, NativeMethods.CFSTR_PERFORMEDDROPEFFECT, 
        StringComparison.OrdinalIgnoreCase) == 0)
    {
        //TODO: Cleanup routines after paste has been performed
    }
    return base.GetData(format, autoConvert);
}

This next method returns a MemoryStream object containing a FILEGROUPDESCRIPTOR structure. Rather than go through the additional complexity of defining a FILEGROUPDESCRIPTOR structure, which is nothing more than an unsigned integer containing the number file descriptors followed by an array of FILEDESCRIPTOR structures, I simply write the descriptor count directly to the memory stream and follow that with the file descriptors.

It is in this method where the data in the array of SelectedItems is required. In order for Windows Explorer to create the target files correctly, it needs to know the name of the file to create and optionally the size and write date. I use the SelectedItems array to pass this information to the class.

Once the file descriptor structure has been populated, it is necessary to write it to the memory stream. This involves some marshaling to convert the structure to a byte array that can then be written to the memory stream. After all of the virtual file descriptors have been created, the method returns the memory stream to the caller:

private MemoryStream GetFileDescriptor(SelectedItem[] SelectedItems)
{
    MemoryStream FileDescriptorMemoryStream = new MemoryStream();
    // Write out the FILEGROUPDESCRIPTOR.cItems value
    FileDescriptorMemoryStream.Write
        (BitConverter.GetBytes(SelectedItems.Length), 0, sizeof(UInt32));
    FILEDESCRIPTOR FileDescriptor = new FILEDESCRIPTOR();
    foreach (SelectedItem si in SelectedItems)
    {
        FileDescriptor.cFileName = si.FileName;
        Int64 FileWriteTimeUtc = si.WriteTime.ToFileTimeUtc();
        FileDescriptor.ftLastWriteTime.dwHighDateTime = 
            (Int32)(FileWriteTimeUtc >> 32);
        FileDescriptor.ftLastWriteTime.dwLowDateTime = 
            (Int32)(FileWriteTimeUtc & 0xFFFFFFFF);
        FileDescriptor.nFileSizeHigh = (UInt32)(si.FileSize >> 32);
        FileDescriptor.nFileSizeLow = (UInt32)(si.FileSize & 0xFFFFFFFF);
        FileDescriptor.dwFlags = NativeMethods.FD_WRITESTIME | 
            NativeMethods.FD_FILESIZE | NativeMethods.FD_PROGRESSUI;
        // Marshal the FileDescriptor structure into a 
        // byte array and write it to the MemoryStream.
        Int32 FileDescriptorSize = Marshal.SizeOf(FileDescriptor);
        IntPtr FileDescriptorPointer = Marshal.AllocHGlobal(FileDescriptorSize);
        Marshal.StructureToPtr(FileDescriptor, FileDescriptorPointer, true);
        Byte[] FileDescriptorByteArray = new Byte[FileDescriptorSize];
        Marshal.Copy(FileDescriptorPointer, 
            FileDescriptorByteArray, 0, FileDescriptorSize);
        Marshal.FreeHGlobal(FileDescriptorPointer);
        FileDescriptorMemoryStream.Write
            (FileDescriptorByteArray, 0, FileDescriptorByteArray.Length);
    }
    return FileDescriptorMemoryStream;
}

This next method returns a memory stream containing the file contents for the file number being requested by Windows Explorer through the FORMATETC field lindex. This method is implementation specific as it requires the virtual file data to be obtained from the virtual file source by whatever means are required to get the data into a byte array. In this code, the SelectedItems array could contain the information needed to render the file from the virtual file system:

private MemoryStream GetFileContents(SelectedItem[] SelectedItems, Int32 FileNumber)
{
    MemoryStream FileContentMemoryStream = null;
    if (SelectedItems != null && FileNumber < SelectedItems.Length)
    {
        FileContentMemoryStream = new MemoryStream();
        SelectedItem si = SelectedItems[FileNumber];
        // ******************************************************************
        // TODO: Get the virtual file contents and place 
        // the contents in the byte array bBuffer.
        // If the contents are zero length then a single byte 
        // must be supplied to Windows
        // Explorer otherwise the transfer will fail.  
        // If this is part of a multi-file transfer,
        // the entire transfer will fail at this point 
        // if the buffer is zero length.
        // ******************************************************************
        Byte[] bBuffer;
                        
        // Must send at least one byte for a zero length file to prevent stoppages.
        if (bBuffer.Length == 0)  
            bBuffer = new Byte[1];
        FileContentMemoryStream.Write(bBuffer, 0, bBuffer.Length);
    }
    return FileContentMemoryStream;
}

These last two methods are used to get a copy of the FORMATETC structure used with the GetData request so that the lindex field can be retrieved for use with a FileContents request. Since there is no override available for this method, it was necessary to replace it entirely by duplicating the .NET Framework version of the method. Using Lutz Roeder's .NET Reflector software, I disassembled the method from System.Windows.Forms.dll and reproduced it in this class taking care to extract the lindex field that I needed. GetTymedUseable is a supporting method used with the GetData method and utilizes the private static ALLOWED_TYMEDS array defined in class definition:

[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
void System.Runtime.InteropServices.ComTypes.IDataObject.GetData
    (ref System.Runtime.InteropServices.ComTypes.FORMATETC formatetc, 
    out System.Runtime.InteropServices.ComTypes.STGMEDIUM medium)
{
    if (formatetc.cfFormat == (Int16)DataFormats.GetFormat
        (NativeMethods.CFSTR_FILECONTENTS).Id)
        m_lindex = formatetc.lindex;
    medium = new System.Runtime.InteropServices.ComTypes.STGMEDIUM();
    if (GetTymedUseable(formatetc.tymed))
    {
        if ((formatetc.tymed & TYMED.TYMED_HGLOBAL) != TYMED.TYMED_NULL)
        {
            medium.tymed = TYMED.TYMED_HGLOBAL;
            medium.unionmember = NativeMethods.GlobalAlloc
            (NativeMethods.GHND | NativeMethods.GMEM_DDESHARE, 1);
            if (medium.unionmember == IntPtr.Zero)
            {
                throw new OutOfMemoryException();
            }
            try
            {
                ((System.Runtime.InteropServices.ComTypes.IDataObject)this).
                GetDataHere(ref formatetc, ref medium);
                return;
            }
            catch
            {
                NativeMethods.GlobalFree(new HandleRef((STGMEDIUM)medium, 
                medium.unionmember));
                medium.unionmember = IntPtr.Zero;
            throw;
            }
        }
        medium.tymed = formatetc.tymed;
        ((System.Runtime.InteropServices.ComTypes.IDataObject)this).
        GetDataHere(ref formatetc, ref medium);
    }
    else
    {
        Marshal.ThrowExceptionForHR(NativeMethods.DV_E_TYMED);
    }
}
private static Boolean GetTymedUseable(TYMED tymed)
{
    for (Int32 i = 0; i < ALLOWED_TYMEDS.Length; i++)
    {
        if ((tymed & ALLOWED_TYMEDS[i]) != TYMED.TYMED_NULL)
        {
            return true;
        }
    }
    return false;
}

Finally, the following code snippet shows a sample implementation using DataObjectEx to create three files with the name My Virtual File, a file size of zero and a write date of January 1, 2008. Since the three files will have the same name, Windows Explorer supplies a file number which will result in the files being named My Virtual File, My Virtual File (1) and My Virtual File (2). Setting the three clipboard formats to null as shown below will enable delayed rendering.

Int32 NumItems = 3;
DataObjectEx.SelectedItem[] SelectedItems = 
    new DataObjectEx.SelectedItem[NumItems];
for (Int32 ItemCount = 0; ItemCount < SelectedItems.Length; ItemCount++)
{
    // TODO: Get virtual file name
    SelectedItems[ItemCount].FileName = "My Virtual File";
    // TODO: Get virtual file date
    SelectedItems[ItemCount].WriteTime = new DateTime(2008, 1, 1);
    // TODO: Get virtual file size
    SelectedItems[ItemCount].FileSize = 0;
}
DataObjectEx dataObject = new DataObjectEx(SelectedItems);
dataObject.SetData(NativeMethods.CFSTR_FILEDESCRIPTORW, null);
dataObject.SetData(NativeMethods.CFSTR_FILECONTENTS, null);
dataObject.SetData(NativeMethods.CFSTR_PERFORMEDDROPEFFECT, null);
Clipboard.SetDataObject(dataObject);

Points of Interest

This was a very interesting project to work on. While working on it, I was not even sure it would be possible to do what I wanted to do. With no examples available anywhere on using these formats, it was truly a "roll your own" scenario. I completed most of the class very quickly but became hung up on getting access to the lindex field of FORMATETC. I spent many head banging sessions trying to find ways to override, peek, subclass, etc. attempting to get access to the structure. It finally became apparently that completely replacing the IDataObject.GetData routine with my own code was the only way to go. Of course that then involved needing to replicate the method as it exists in the .NET Framework. Thankfully there are tools such as Reflector that allow for the disassembly of .NET Framework code so that one can learn how to do certain interesting things!

History

  • V1.0 - January 23, 2008: Initial release
  • V1.1 - January 28, 2008: Changed list of valid TYMED's and minor FxCop suggested changes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

n2jtx
Software Developer (Senior)
United States United States
a.k.a. Robert G. Schaffrath
 
I am a programmer and system administrator with a teeny tiny software company on Long Island and I also do some freelance programming work.
 
I started programming in High School with Basic-Plus on a timeshared DEC PDP-11/70 running RSTS/E back in the late 1970's. Next I worked with various languages under DEC VAX/VMS and Data General AOS/VS in the 1980's. That was followed by C and Perl under various flavors of Unix. On the PC side I worked with Turbo Pascal and Turbo C under DOS and eventually migrated to Visual C and Visual Basic under Windows. I have been working with C# and .NET for a over six years now and have been doing a lot of "Interop" work between native code and .NET.
 
In my spare time I am an Amateur Radio operator and I also volunteer with our local Community Emergency Response Team (CERT) and serve on the Board of Directors for an organization that provides an educational enrichment program for children in need of after-school care and
instruction. In addition, I like to brew beer and bicycle.

Comments and Discussions

 
GeneralDropping Virtual Files from listview to Desktop/Explorer PinmemberMember 450441420-Oct-10 3:52 
QuestionHow can I get explorer to skip Shell IDList Array? Pinmemberseabasstke26-Aug-10 12:04 
GeneralProblems with NGEN PinmemberCrazyTn24-Jan-10 20:24 
GeneralFile size 0 bytes PinmemberToon Geens16-Dec-09 4:26 
GeneralFolders Pinmembermike payne17-Sep-09 23:57 
GeneralRe: Folders Pinmembern2jtx18-Sep-09 6:52 
GeneralRe: Folders [modified] Pinmembermike payne19-Sep-09 4:19 
GeneralRe: Folders Pinmembermike payne20-Sep-09 6:20 
GeneralRe: Folders Pinmembermike payne20-Sep-09 8:12 
GeneralRe: Folders PinmemberMember 450441420-Oct-10 2:40 
AnswerRe: Folders [modified] Pinmembermy.hegemon26-Apr-10 22:14 
GeneralRe: Folders Pinmemberhhy12313-Sep-10 23:33 
GeneralRe: Folders Pinmembermatieu10-Feb-14 11:14 
QuestionSaving Files to The Virtual Drive Pinmemberstixoffire18-Apr-09 0:14 
GeneralSeveral questions. PinmvpGiorgi Dalakishvili24-Nov-08 21:47 
QuestionDelayed Rendering? Pinmemberyarborg14-Oct-08 5:30 
QuestionQuestion about modifying your implementation in GetFileContents() [modified] PinmemberGreg Cadmes9-Apr-08 12:22 
QuestionLimitation? Pinmemberbwaide3-Mar-08 5:21 
QuestionLarge files & IStream PinmemberMember 279387011-Feb-08 16:29 
GeneralRe: Large files & IStream Pinmembern2jtx12-Feb-08 7:44 
GeneralRe: Large files & IStream PinmemberDarkmoor12-Feb-08 8:09 
GeneralRe: Large files & IStream PinmemberDarkmoor15-Feb-08 4:01 
GeneralRe: Large files & IStream Pinmembern2jtx16-Feb-08 6:20 
GeneralRe: Large files & IStream PinmemberDarkmoor16-Feb-08 7:56 
GeneralRe: Large files & IStream [modified] PinmvpGiorgi Dalakishvili24-Nov-08 21:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141216.1 | Last Updated 28 Jan 2008
Article Copyright 2008 by n2jtx
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid