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 namespace
s 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);
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";
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;
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;
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)
{
}
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();
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;
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];
Byte[] bBuffer;
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++)
{
SelectedItems[ItemCount].FileName = "My Virtual File";
SelectedItems[ItemCount].WriteTime = new DateTime(2008, 1, 1);
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