Introduction
A project came up at work the other day requiring the ability to drag and drop any number of mail messages or mail message attachments from Otlook into a WinForms application... Easy I thought, this has to be a common problem. I will just jump on CodeProject, find an example, and with a bit of tweaking, be running in no time. Now, if you are here, I am sure you now know how little information there is available on this topic, so I thought I would add to the very small pool with a complete example of how to drag and drop mail items or attachments from Outlook into a WinForms application without using the Outlook object model.
I am going to skip over explaining all the creating a form, allowing drag and drop, etc., stuff that you can find a million places on the Internet, and just focus on the code to make drag and drop from Outlook work. If you feel lost, go find another article with more basic drag and drop information, get a fully functional drag and drop app, then come back here and work from that code.
Using the Code
When I started writing the code, I decided the easiest way to work the functionality into the existing application was to create a new class that implemented the IDataObject
interface that is normally provided when dragging and dropping onto a WinForm. The new class was to catch any calls to Outlook specific data formats, and pass all other calls through to the original IDataObject
. Below, you can see how easy it is to use the class in a DragDrop
event handler. The FileGroupDescriptor
format returns a string array containing the names of each file dropped instead of the usual MemoryStream
you would be used to if you have tackled this yourself, and the FileContents
returns a MemoryStream
array containing the binary contents of each file dropped.
private void Form1_DragDrop(object sender, DragEventArgs e)
{
OutlookDataObject dataObject = new OutlookDataObject(e.Data);
string[] filenames = (string[])dataObject.GetData("FileGroupDescriptor");
MemoryStream[] filestreams = (MemoryStream[])dataObject.GetData("FileContents");
for (int fileIndex = 0; fileIndex < filenames.Length; fileIndex++)
{
string filename = filenames[fileIndex];
MemoryStream filestream = filestreams[fileIndex];
FileStream outputStream = File.Create(filename);
filestream.WriteTo(outputStream);
outputStream.Close();
}
}
Understanding the Code
To understand what the OutlookDataObject
class above is doing to get the file information, there are two things to take note of. The first is that information for the file names is returned from Outlook in a MemoryStream
, which is actually a representation of the FILEGROUPDESCRIPTORA
or FILEGROUPDESCRIPTORW
structures. The second is that to get the file contents, you need the ability to specify an index to get anything past the first file, and the standard IDataObject
does not expose this ability. All this is explained in detail below.
Getting the File Names
There are two versions of the file details returned from the IDataObject
in the FileGroupDescriptor
and FileGroupDescriptorW
formats which map to the FILEGROUPDESCRIPTORA
and FILEGROUPDESCRIPTORW
structures, respectively. In this article, I will focus on the FileGroupDescriptor
format which is the ASCII version; FileGroupDescriptorW
(W for wide) is the Unicode version, and you will need to use it when working with non-ASCII file names, but they are handled in the same way.
MemoryStream fileGroupDescriptorStream = (MemoryStream)e.Data.GetData("FileGroupDescriptor");
Most examples you will see involve taking the MemoryStream
above and converting each non-null byte from index 76 onwards to a char and appending that to a string. While this works adequately for one file drop, it gets a bit tricky when dropping more than that. The correct way is to take the returned bytes and cast it to a FILEGROUPDESCRIPTORA
structure, which holds a count of items and an array of FILEDESCRIPTORA
structures, which holds the file details. The definitions of these structures can be seen below.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEGROUPDESCRIPTORA
{
public uint cItems;
public FILEDESCRIPTORA[] fgd;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEDESCRIPTORA
{
public uint dwFlags;
public Guid clsid;
public SIZEL sizel;
public POINTL pointl;
public uint dwFileAttributes;
public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
}
Now that the foundations are set, let's get into the code to actually convert the returned MemoryStream
into something usable like a string array of file names. This involves putting the raw bytes into unmanaged memory and using Marshal.PtrToStructure
to bring it back in as a structure. There is some extra marshalling in the code because the fgd
array of the FILEGROUPDESCRIPTORA
structure doesn't get populated as the PtrToStructure
method doesn't work with variable length arrays.
MemoryStream fgdStream =
(MemoryStream)e.Data.GetData("FileGroupDescriptor");
byte[] fgdBytes = new byte[fgdStream.Length];
fgdStream.Read(fgdBytes, 0, fgdBytes.Length);
fgdStream.Close();
IntPtr fgdaPtr = Marshal.AllocHGlobal(fgdBytes.Length);
Marshal.Copy(fgdBytes, 0, fgdaPtr, fgdBytes.Length);
object fgdObj = Marshal.PtrToStructure(fgdaPtr,
typeof(NativeMethods.FILEGROUPDESCRIPTORA));
NativeMethods.FILEGROUPDESCRIPTORA fgd =
(NativeMethods.FILEGROUPDESCRIPTORA)fgdObj;
string[] fileNames = new string[fgd.cItems];
IntPtr fdPtr = (IntPtr)((int)fgdaPointer + Marshal.SizeOf(fgdaPointer));
for(int fdIndex = 0;fdIndex < fgd.cItems;fdIndex++)
{
object fdObj = Marshal.PtrToStructure(fdPtr,
typeof(NativeMethods.FILEDESCRIPTORA));
NativeMethods.FILEDESCRIPTORA fd = (NativeMethods.FILEDESCRIPTORA)fdObj;
fileNames[fdIndex] = fd.cFileName;
fdPtr = (IntPtr)((int)fdPtr + Marshal.SizeOf(fd));
}
At this point, we have now converted the MemoryStream
into a string array that contains the name of each file dropped, which is a lot easier to work with. Outlook messages get the name of their subject with ".msg" on the end, and for Outlook message attachments, the file name of the attachment.
Getting the File Contents
The file contents sit behind the FileContents
format. If you drag and drop a single attachment, then the default IDataObject
works as expected and will return a MemoryStream
containing that file's data. Things get more complex when dragging multiple attachments or Outlook email messages for different reasons. Multiple attachments pose an issue because the OS calls for drop data allow for an index to be specified, but the C# implementation of the IDataObject
doesn't expose this directly. Mail messages are an issue because the OS call returns an IStorage
which is a compound file type, and again, the C# implementation of the IDataObject
lets us down by not handling this type of return, so you get a null
.
Specifying an Index
To get at the content of multiple dropped files, an index needs to be specified to indicate which file contents are required. The default IDataObject
doesn't allow this, but it can be cast to a COM IDataObject
which will accept a FORMATETC
structure that has an index property that can be set to indicate the file contents required.
System.Runtime.InteropServices.ComTypes.IDataObject comDataObject;
comDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)e.Data;
FORMATETC formatetc = new FORMATETC();
formatetc.cfFormat = (short)DataFormats.GetFormat(format).Id;
formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
formatetc.lindex = 0;
formatetc.ptd = new IntPtr(0);
formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL;
STGMEDIUM medium = new STGMEDIUM();
comDataObject.GetData(ref formatetc, out medium);
As you can see in the example above, by changing the value of the lindex
property of the FORMATETC
structure, we can change the index of the file contents to retrieve. The result of the call is sitting in the STGMEDIUM
structure; this contains a pointer to the actual result in the unionmember
property, and the type of result at the pointer in the tymed
property. There are three types of returns available to the STGMEDIUM
, and each one is explained below.
The Stream Result (TYMED_ISTREAM)
If the tymed
property of the STGMEDIUM
is TYMED_ISTREAM
, then the result is a stream. This is normally handled by the default IDataObject
, but when working with the COM IDataObject
, the handling code needs to be written again.
IStream iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
Marshal.Release(medium.unionmember);
iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
iStream.Stat(out iStreamStat, 0);
int iStreamSize = (int)iStreamStat.cbSize;
byte[] iStreamContent = new byte[iStreamSize];
iStream.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);
Stream filestream = new MemoryStream(iStreamContent);
The Storage Result (TYMED_ISTORAGE)
If the tymed
property of the STGMEDIUM
is TYMED_ISTORAGE
, then the result is a storage which is a compound file type. This is a little more complex to process than the stream as it needs to be copied into a memory backed IStorage
so its data can then be read from the backing memory store.
NativeMethods.IStorage iStorage = null;
NativeMethods.IStorage iStorage2 = null;
NativeMethods.ILockBytes iLockBytes = null;
System.Runtime.InteropServices.ComTypes.STATSTG iLockBytesStat;
try
{
iStorage = (NativeMethods.IStorage)
Marshal.GetObjectForIUnknown(medium.unionmember);
Marshal.Release(medium.unionmember);
iLockBytes = NativeMethods.CreateILockBytesOnHGlobal(IntPtr.Zero, true);
iStorage2 = NativeMethods.StgCreateDocfileOnILockBytes(iLockBytes,
0x00001012, 0);
iStorage.CopyTo(0, null, IntPtr.Zero, iStorage2);
iLockBytes.Flush();
iStorage2.Commit(0);
iLockBytesStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
iLockBytes.Stat(out iLockBytesStat, 1);
int iLockBytesSize = (int)iLockBytesStat.cbSize;
byte[] iLockBytesContent = new byte[iLockBytesSize];
iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Length, null);
Stream filestream = new MemoryStream(iStreamContent);
}
finally
{
Marshal.ReleaseComObject(iStorage2);
Marshal.ReleaseComObject(iLockBytes);
Marshal.ReleaseComObject(iStorage);
}
The HGlobal Result (TYMED_HGLOBAL)
If the tymed
property of the STGMEDIUM
is TYMED_HGLOBAL
, then the result is stored in a HGlobal
. For the purposes of Outlook drag and drop, this type should never be returned, but for completeness, I use a little bit of Reflection on the original IDataObject
to have that class handle it.
BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo innerDataField =
e.Data.GetType().GetField("innerData", bindingFlags);
IDataObject oleDataObject =
(System.Windows.Forms.IDataObject)innerDataField.GetValue(e.Data);
MethodInfo getDataFromHGLOBLALMethod =
oleDataObject.GetType().GetMethod("GetDataFromHGLOBLAL", bindingFlags);
getDataFromHGLOBLALMethod.Invoke(oleDataObject,
new object[] { format, medium.unionmember });
Conclusion
Well, hopefully, that all helps someone. I have a few other Outlook tricks that I will be doing articles for; one is how to extract and save a message attachment without using the object model, another is how to use the code in this article to enable drag and drop of Outlook messages and attachments into IE (with appropriate security, so only good for the intranet).
History