Bläddra i källkod

Added Drag-Drop functionality from emails and files, extracting that functionality from PRS DataEntry screen.

Kenric Nugteren 1 år sedan
förälder
incheckning
f16018d044

+ 598 - 0
inabox.wpf/DocumentUtils.cs

@@ -0,0 +1,598 @@
+using Syncfusion.Pdf;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace InABox.Wpf;
+
+public static class DocumentUtils
+{
+    public static IEnumerable<Tuple<string, Stream?>>? HandleFileDrop(DragEventArgs e)
+    {
+        var dataObject = new OutlookDataObject(e.Data);
+
+        string? desc = null;
+        if (dataObject.GetDataPresent("FileGroupDescriptor")) desc = "FileGroupDescriptor";
+        else if (dataObject.GetDataPresent("FileGroupDescriptorW")) desc = "FileGroupDescriptorW";
+
+        if (desc is not null)
+        {
+            var result = new List<Tuple<string, Stream?>>();
+            var filenames = (string[])dataObject.GetData(desc);
+            var filestreams = (MemoryStream[])dataObject.GetData("FileContents");
+            for (var i = 0; i < filenames.Length; i++)
+            {
+                var filename = filenames[i];
+                var filestream = filestreams[i];
+                result.Add(new Tuple<string, Stream?>(filename, filestream));
+            }
+            return result;
+        }
+        else if (dataObject.GetDataPresent(DataFormats.FileDrop))
+        {
+            var result = new List<Tuple<string, Stream?>>();
+            foreach (var filename in (string[])dataObject.GetData(DataFormats.FileDrop))
+            {
+                if (File.Exists(filename))
+                {
+                    result.Add(new Tuple<string, Stream?>(filename, null));
+                }
+            }
+            return result;
+        }
+        return null;
+    }
+
+
+    // https://gist.github.com/MattyBoy4444/521547
+    public class OutlookDataObject : System.Windows.IDataObject
+    {
+        #region NativeMethods
+
+        private class NativeMethods
+        {
+            [DllImport("kernel32.dll")]
+            static extern IntPtr GlobalLock(IntPtr hMem);
+
+            [DllImport("ole32.dll", PreserveSig = false)]
+            public static extern ILockBytes CreateILockBytesOnHGlobal(IntPtr hGlobal, bool fDeleteOnRelease);
+
+            [DllImport("OLE32.DLL", CharSet = CharSet.Auto, PreserveSig = false)]
+            public static extern IntPtr GetHGlobalFromILockBytes(ILockBytes pLockBytes);
+
+            [DllImport("OLE32.DLL", CharSet = CharSet.Unicode, PreserveSig = false)]
+            public static extern IStorage StgCreateDocfileOnILockBytes(ILockBytes plkbyt, uint grfMode, uint reserved);
+
+            [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000000B-0000-0000-C000-000000000046")]
+            public interface IStorage
+            {
+                [return: MarshalAs(UnmanagedType.Interface)]
+                IStream CreateStream([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved1, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
+                [return: MarshalAs(UnmanagedType.Interface)]
+                IStream OpenStream([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, IntPtr reserved1, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
+                [return: MarshalAs(UnmanagedType.Interface)]
+                IStorage CreateStorage([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.U4)] int grfMode, [In, MarshalAs(UnmanagedType.U4)] int reserved1, [In, MarshalAs(UnmanagedType.U4)] int reserved2);
+                [return: MarshalAs(UnmanagedType.Interface)]
+                IStorage OpenStorage([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, IntPtr pstgPriority, [In, MarshalAs(UnmanagedType.U4)] int grfMode, IntPtr snbExclude, [In, MarshalAs(UnmanagedType.U4)] int reserved);
+                void CopyTo(int ciidExclude, [In, MarshalAs(UnmanagedType.LPArray)] Guid[] pIIDExclude, IntPtr snbExclude, [In, MarshalAs(UnmanagedType.Interface)] IStorage stgDest);
+                void MoveElementTo([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In, MarshalAs(UnmanagedType.Interface)] IStorage stgDest, [In, MarshalAs(UnmanagedType.BStr)] string pwcsNewName, [In, MarshalAs(UnmanagedType.U4)] int grfFlags);
+                void Commit(int grfCommitFlags);
+                void Revert();
+                void EnumElements([In, MarshalAs(UnmanagedType.U4)] int reserved1, IntPtr reserved2, [In, MarshalAs(UnmanagedType.U4)] int reserved3, [MarshalAs(UnmanagedType.Interface)] out object ppVal);
+                void DestroyElement([In, MarshalAs(UnmanagedType.BStr)] string pwcsName);
+                void RenameElement([In, MarshalAs(UnmanagedType.BStr)] string pwcsOldName, [In, MarshalAs(UnmanagedType.BStr)] string pwcsNewName);
+                void SetElementTimes([In, MarshalAs(UnmanagedType.BStr)] string pwcsName, [In] System.Runtime.InteropServices.ComTypes.FILETIME pctime, [In] System.Runtime.InteropServices.ComTypes.FILETIME patime, [In] System.Runtime.InteropServices.ComTypes.FILETIME pmtime);
+                void SetClass([In] ref Guid clsid);
+                void SetStateBits(int grfStateBits, int grfMask);
+                void Stat([Out] out System.Runtime.InteropServices.ComTypes.STATSTG pStatStg, int grfStatFlag);
+            }
+
+            [ComImport, Guid("0000000A-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+            public interface ILockBytes
+            {
+                void ReadAt([In, MarshalAs(UnmanagedType.U8)] long ulOffset, [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] byte[] pv, [In, MarshalAs(UnmanagedType.U4)] int cb, [Out, MarshalAs(UnmanagedType.LPArray)] int[] pcbRead);
+                void WriteAt([In, MarshalAs(UnmanagedType.U8)] long ulOffset, IntPtr pv, [In, MarshalAs(UnmanagedType.U4)] int cb, [Out, MarshalAs(UnmanagedType.LPArray)] int[] pcbWritten);
+                void Flush();
+                void SetSize([In, MarshalAs(UnmanagedType.U8)] long cb);
+                void LockRegion([In, MarshalAs(UnmanagedType.U8)] long libOffset, [In, MarshalAs(UnmanagedType.U8)] long cb, [In, MarshalAs(UnmanagedType.U4)] int dwLockType);
+                void UnlockRegion([In, MarshalAs(UnmanagedType.U8)] long libOffset, [In, MarshalAs(UnmanagedType.U8)] long cb, [In, MarshalAs(UnmanagedType.U4)] int dwLockType);
+                void Stat([Out] out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, [In, MarshalAs(UnmanagedType.U4)] int grfStatFlag);
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            public sealed class POINTL
+            {
+                public int x;
+                public int y;
+            }
+
+            [StructLayout(LayoutKind.Sequential)]
+            public sealed class SIZEL
+            {
+                public int cx;
+                public int cy;
+            }
+
+            [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;
+            }
+
+            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+            public sealed class FILEGROUPDESCRIPTORW
+            {
+                public uint cItems;
+                public FILEDESCRIPTORW fgd;
+            }
+
+            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+            public sealed class FILEDESCRIPTORW
+            {
+                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;
+            }
+        }
+
+        #endregion
+
+        #region Property(s)
+
+        /// <summary>
+        /// Holds the <see cref="System.Windows.IDataObject"/> that this class is wrapping
+        /// </summary>
+        private System.Windows.IDataObject underlyingDataObject;
+
+        /// <summary>
+        /// Holds the <see cref="System.Runtime.InteropServices.ComTypes.IDataObject"/> interface to the <see cref="System.Windows.IDataObject"/> that this class is wrapping.
+        /// </summary>
+        private System.Runtime.InteropServices.ComTypes.IDataObject comUnderlyingDataObject;
+
+        /// <summary>
+        /// Holds the internal ole <see cref="System.Windows.IDataObject"/> to the <see cref="System.Windows.IDataObject"/> that this class is wrapping.
+        /// </summary>
+        private System.Windows.IDataObject oleUnderlyingDataObject;
+
+        /// <summary>
+        /// Holds the <see cref="MethodInfo"/> of the "GetDataFromHGLOBAL" method of the internal ole <see cref="System.Windows.IDataObject"/>.
+        /// </summary>
+        private MethodInfo getDataFromHGLOBALMethod;
+
+        #endregion
+
+        #region Constructor(s)
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OutlookDataObject"/> class.
+        /// </summary>
+        /// <param name="underlyingDataObject">The underlying data object to wrap.</param>
+        public OutlookDataObject(System.Windows.IDataObject underlyingDataObject)
+        {
+            //get the underlying dataobject and its ComType IDataObject interface to it
+            this.underlyingDataObject = underlyingDataObject;
+            this.comUnderlyingDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)this.underlyingDataObject;
+
+            //get the internal ole dataobject and its GetDataFromHGLOBAL so it can be called later
+            FieldInfo innerDataField = this.underlyingDataObject.GetType().GetField("_innerData", BindingFlags.NonPublic | BindingFlags.Instance);
+            this.oleUnderlyingDataObject = (System.Windows.IDataObject)innerDataField.GetValue(this.underlyingDataObject);
+            this.getDataFromHGLOBALMethod = this.oleUnderlyingDataObject.GetType().GetMethod("GetDataFromHGLOBAL", BindingFlags.NonPublic | BindingFlags.Instance);
+        }
+
+        #endregion
+
+        #region IDataObject Members
+
+        /// <summary>
+        /// Retrieves the data associated with the specified class type format.
+        /// </summary>
+        /// <param name="format">A <see cref="T:System.Type"></see> representing the format of the data to retrieve. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <returns>
+        /// The data associated with the specified format, or null.
+        /// </returns>
+        public object GetData(Type format)
+        {
+            return this.GetData(format.FullName);
+        }
+
+        /// <summary>
+        /// Retrieves the data associated with the specified data format.
+        /// </summary>
+        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <returns>
+        /// The data associated with the specified format, or null.
+        /// </returns>
+        public object GetData(string format)
+        {
+            return this.GetData(format, true);
+        }
+
+        /// <summary>
+        /// Retrieves the data associated with the specified data format, using a Boolean to determine whether to convert the data to the format.
+        /// </summary>
+        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="autoConvert">true to convert the data to the specified format; otherwise, false.</param>
+        /// <returns>
+        /// The data associated with the specified format, or null.
+        /// </returns>
+        public object GetData(string format, bool autoConvert)
+        {
+            //handle the "FileGroupDescriptor" and "FileContents" format request in this class otherwise pass through to underlying IDataObject 
+            switch (format)
+            {
+                case "FileGroupDescriptor":
+                    //override the default handling of FileGroupDescriptor which returns a
+                    //MemoryStream and instead return a string array of file names
+                    IntPtr fileGroupDescriptorAPointer = IntPtr.Zero;
+                    try
+                    {
+                        //use the underlying IDataObject to get the FileGroupDescriptor as a MemoryStream
+                        MemoryStream fileGroupDescriptorStream = (MemoryStream)this.underlyingDataObject.GetData("FileGroupDescriptor", autoConvert);
+                        byte[] fileGroupDescriptorBytes = new byte[fileGroupDescriptorStream.Length];
+                        fileGroupDescriptorStream.Read(fileGroupDescriptorBytes, 0, fileGroupDescriptorBytes.Length);
+                        fileGroupDescriptorStream.Close();
+
+                        //copy the file group descriptor into unmanaged memory 
+                        fileGroupDescriptorAPointer = Marshal.AllocHGlobal(fileGroupDescriptorBytes.Length);
+                        Marshal.Copy(fileGroupDescriptorBytes, 0, fileGroupDescriptorAPointer, fileGroupDescriptorBytes.Length);
+
+                        ////marshal the unmanaged memory to to FILEGROUPDESCRIPTORA struct
+                        //FIX FROM - https://stackoverflow.com/questions/27173844/accessviolationexception-after-copying-a-file-from-inside-a-zip-archive-to-the-c
+                        int ITEMCOUNT = Marshal.ReadInt32(fileGroupDescriptorAPointer);
+
+                        //create a new array to store file names in of the number of items in the file group descriptor
+                        string[] fileNames = new string[ITEMCOUNT];
+
+                        //get the pointer to the first file descriptor
+                        IntPtr fileDescriptorPointer = (IntPtr)((long)fileGroupDescriptorAPointer + Marshal.SizeOf(ITEMCOUNT));
+
+                        //loop for the number of files acording to the file group descriptor
+                        for (int fileDescriptorIndex = 0; fileDescriptorIndex < ITEMCOUNT; fileDescriptorIndex++)
+                        {
+                            //marshal the pointer top the file descriptor as a FILEDESCRIPTORA struct and get the file name
+                            NativeMethods.FILEDESCRIPTORA fileDescriptor = (NativeMethods.FILEDESCRIPTORA)Marshal.PtrToStructure(fileDescriptorPointer, typeof(NativeMethods.FILEDESCRIPTORA));
+                            fileNames[fileDescriptorIndex] = fileDescriptor.cFileName;
+
+                            //move the file descriptor pointer to the next file descriptor
+                            fileDescriptorPointer = (IntPtr)((long)fileDescriptorPointer + Marshal.SizeOf(fileDescriptor));
+                        }
+
+                        //return the array of filenames
+                        return fileNames;
+                    }
+                    finally
+                    {
+                        //free unmanaged memory pointer
+                        Marshal.FreeHGlobal(fileGroupDescriptorAPointer);
+                    }
+
+                case "FileGroupDescriptorW":
+                    //override the default handling of FileGroupDescriptorW which returns a
+                    //MemoryStream and instead return a string array of file names
+                    IntPtr fileGroupDescriptorWPointer = IntPtr.Zero;
+                    try
+                    {
+                        //use the underlying IDataObject to get the FileGroupDescriptorW as a MemoryStream
+                        MemoryStream fileGroupDescriptorStream = (MemoryStream)this.underlyingDataObject.GetData("FileGroupDescriptorW");
+                        byte[] fileGroupDescriptorBytes = new byte[fileGroupDescriptorStream.Length];
+                        fileGroupDescriptorStream.Read(fileGroupDescriptorBytes, 0, fileGroupDescriptorBytes.Length);
+                        fileGroupDescriptorStream.Close();
+
+                        //copy the file group descriptor into unmanaged memory
+                        fileGroupDescriptorWPointer = Marshal.AllocHGlobal(fileGroupDescriptorBytes.Length);
+                        Marshal.Copy(fileGroupDescriptorBytes, 0, fileGroupDescriptorWPointer, fileGroupDescriptorBytes.Length);
+
+                        //marshal the unmanaged memory to to FILEGROUPDESCRIPTORW struct
+                        //FIX FROM - https://stackoverflow.com/questions/27173844/accessviolationexception-after-copying-a-file-from-inside-a-zip-archive-to-the-c
+                        int ITEMCOUNT = Marshal.ReadInt32(fileGroupDescriptorWPointer);
+
+                        //create a new array to store file names in of the number of items in the file group descriptor
+                        string[] fileNames = new string[ITEMCOUNT];
+
+                        //get the pointer to the first file descriptor
+                        IntPtr fileDescriptorPointer = (IntPtr)((long)fileGroupDescriptorWPointer + Marshal.SizeOf(ITEMCOUNT));
+
+                        //loop for the number of files acording to the file group descriptor
+                        for (int fileDescriptorIndex = 0; fileDescriptorIndex < ITEMCOUNT; fileDescriptorIndex++)
+                        {
+                            //marshal the pointer top the file descriptor as a FILEDESCRIPTORW struct and get the file name
+                            NativeMethods.FILEDESCRIPTORW fileDescriptor = (NativeMethods.FILEDESCRIPTORW)Marshal.PtrToStructure(fileDescriptorPointer, typeof(NativeMethods.FILEDESCRIPTORW));
+                            fileNames[fileDescriptorIndex] = fileDescriptor.cFileName;
+
+                            //move the file descriptor pointer to the next file descriptor
+                            fileDescriptorPointer = (IntPtr)((long)fileDescriptorPointer + Marshal.SizeOf(fileDescriptor));
+                        }
+
+                        //return the array of filenames
+                        return fileNames;
+                    }
+                    finally
+                    {
+                        //free unmanaged memory pointer
+                        Marshal.FreeHGlobal(fileGroupDescriptorWPointer);
+                    }
+
+                case "FileContents":
+                    //override the default handling of FileContents which returns the
+                    //contents of the first file as a memory stream and instead return                    
+                    //a array of MemoryStreams containing the data to each file dropped                    
+                    //
+                    // FILECONTENTS requires a companion FILEGROUPDESCRIPTOR to be                     
+                    // available so we bail out if we don't find one in the data object.
+
+                    string fgdFormatName;
+                    if (GetDataPresent("FileGroupDescriptorW"))
+                        fgdFormatName = "FileGroupDescriptorW";
+                    else if (GetDataPresent("FileGroupDescriptor"))
+                        fgdFormatName = "FileGroupDescriptor";
+                    else
+                        return null;
+                    //get the array of filenames which lets us know how many file contents exist                    
+                    string[] fileContentNames = (string[])this.GetData(fgdFormatName);
+
+                    //create a MemoryStream array to store the file contents
+                    MemoryStream[] fileContents = new MemoryStream[fileContentNames.Length];
+
+                    //loop for the number of files acording to the file names
+                    for (int fileIndex = 0; fileIndex < fileContentNames.Length; fileIndex++)
+                    {
+                        //get the data at the file index and store in array
+                        fileContents[fileIndex] = this.GetData(format, fileIndex);
+                    }
+
+                    //return array of MemoryStreams containing file contents
+                    return fileContents;
+            }
+
+            //use underlying IDataObject to handle getting of data
+            return this.underlyingDataObject.GetData(format, autoConvert);
+        }
+
+        /// <summary>
+        /// Retrieves the data associated with the specified data format at the specified index.
+        /// </summary>
+        /// <param name="format">The format of the data to retrieve. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="index">The index of the data to retrieve.</param>
+        /// <returns>
+        /// A <see cref="MemoryStream"/> containing the raw data for the specified data format at the specified index.
+        /// </returns>
+        public MemoryStream GetData(string format, int index)
+        {
+            //create a FORMATETC struct to request the data with
+            FORMATETC formatetc = new FORMATETC();
+            formatetc.cfFormat = (short)DataFormats.GetDataFormat(format).Id;
+            formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
+            formatetc.lindex = index;
+            formatetc.ptd = new IntPtr(0);
+            formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL;
+
+            //create STGMEDIUM to output request results into
+            STGMEDIUM medium = new STGMEDIUM();
+
+            //using the Com IDataObject interface get the data using the defined FORMATETC
+            this.comUnderlyingDataObject.GetData(ref formatetc, out medium);
+
+            //retrieve the data depending on the returned store type
+            switch (medium.tymed)
+            {
+                case TYMED.TYMED_ISTORAGE:
+                    //to handle a IStorage it needs to be written into a second unmanaged
+                    //memory mapped storage and then the data can be read from memory into
+                    //a managed byte and returned as a MemoryStream
+
+                    NativeMethods.IStorage iStorage = null;
+                    NativeMethods.IStorage iStorage2 = null;
+                    NativeMethods.ILockBytes iLockBytes = null;
+                    System.Runtime.InteropServices.ComTypes.STATSTG iLockBytesStat;
+                    try
+                    {
+                        //marshal the returned pointer to a IStorage object
+                        iStorage = (NativeMethods.IStorage)Marshal.GetObjectForIUnknown(medium.unionmember);
+                        Marshal.Release(medium.unionmember);
+
+                        //create a ILockBytes (unmanaged byte array) and then create a IStorage using the byte array as a backing store
+                        iLockBytes = NativeMethods.CreateILockBytesOnHGlobal(IntPtr.Zero, true);
+                        iStorage2 = NativeMethods.StgCreateDocfileOnILockBytes(iLockBytes, 0x00001012, 0);
+
+                        //copy the returned IStorage into the new IStorage
+                        iStorage.CopyTo(0, null, IntPtr.Zero, iStorage2);
+                        iLockBytes.Flush();
+                        iStorage2.Commit(0);
+
+                        //get the STATSTG of the ILockBytes to determine how many bytes were written to it
+                        iLockBytesStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
+                        iLockBytes.Stat(out iLockBytesStat, 1);
+                        int iLockBytesSize = (int)iLockBytesStat.cbSize;
+
+                        //read the data from the ILockBytes (unmanaged byte array) into a managed byte array
+                        byte[] iLockBytesContent = new byte[iLockBytesSize];
+                        iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Length, null);
+
+                        //wrapped the managed byte array into a memory stream and return it
+                        return new MemoryStream(iLockBytesContent);
+                    }
+                    finally
+                    {
+                        //release all unmanaged objects
+                        Marshal.ReleaseComObject(iStorage2);
+                        Marshal.ReleaseComObject(iLockBytes);
+                        Marshal.ReleaseComObject(iStorage);
+                    }
+
+                case TYMED.TYMED_ISTREAM:
+                    //to handle a IStream it needs to be read into a managed byte and
+                    //returned as a MemoryStream
+
+                    IStream iStream = null;
+                    System.Runtime.InteropServices.ComTypes.STATSTG iStreamStat;
+                    try
+                    {
+                        //marshal the returned pointer to a IStream object
+                        iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
+                        Marshal.Release(medium.unionmember);
+
+                        //get the STATSTG of the IStream to determine how many bytes are in it
+                        iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
+                        iStream.Stat(out iStreamStat, 0);
+                        int iStreamSize = (int)iStreamStat.cbSize;
+
+                        //read the data from the IStream into a managed byte array
+                        byte[] iStreamContent = new byte[iStreamSize];
+                        iStream.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);
+
+                        //wrapped the managed byte array into a memory stream and return it
+                        return new MemoryStream(iStreamContent);
+                    }
+                    finally
+                    {
+                        //release all unmanaged objects
+                        Marshal.ReleaseComObject(iStream);
+                    }
+
+                case TYMED.TYMED_HGLOBAL:
+                    //to handle a HGlobal the exisitng "GetDataFromHGLOBAL" method is invoked via
+                    //reflection
+
+                    return (MemoryStream)this.getDataFromHGLOBALMethod.Invoke(this.oleUnderlyingDataObject, new object[] { DataFormats.GetDataFormat((short)formatetc.cfFormat).Name, medium.unionmember });
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Determines whether data stored in this instance is associated with, or can be converted to, the specified format.
+        /// </summary>
+        /// <param name="format">A <see cref="T:System.Type"></see> representing the format for which to check. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <returns>
+        /// true if data stored in this instance is associated with, or can be converted to, the specified format; otherwise, false.
+        /// </returns>
+        public bool GetDataPresent(Type format)
+        {
+            return this.underlyingDataObject.GetDataPresent(format);
+        }
+
+        /// <summary>
+        /// Determines whether data stored in this instance is associated with, or can be converted to, the specified format.
+        /// </summary>
+        /// <param name="format">The format for which to check. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <returns>
+        /// true if data stored in this instance is associated with, or can be converted to, the specified format; otherwise false.
+        /// </returns>
+        public bool GetDataPresent(string format)
+        {
+            return this.underlyingDataObject.GetDataPresent(format);
+        }
+
+        /// <summary>
+        /// Determines whether data stored in this instance is associated with the specified format, using a Boolean value to determine whether to convert the data to the format.
+        /// </summary>
+        /// <param name="format">The format for which to check. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="autoConvert">true to determine whether data stored in this instance can be converted to the specified format; false to check whether the data is in the specified format.</param>
+        /// <returns>
+        /// true if the data is in, or can be converted to, the specified format; otherwise, false.
+        /// </returns>
+        public bool GetDataPresent(string format, bool autoConvert)
+        {
+            return this.underlyingDataObject.GetDataPresent(format, autoConvert);
+        }
+
+        /// <summary>
+        /// Returns a list of all formats that data stored in this instance is associated with or can be converted to.
+        /// </summary>
+        /// <returns>
+        /// An array of the names that represents a list of all formats that are supported by the data stored in this object.
+        /// </returns>
+        public string[] GetFormats()
+        {
+            return this.underlyingDataObject.GetFormats();
+        }
+
+        /// <summary>
+        /// Gets a list of all formats that data stored in this instance is associated with or can be converted to, using a Boolean value to determine whether to retrieve all formats that the data can be converted to or only native data formats.
+        /// </summary>
+        /// <param name="autoConvert">true to retrieve all formats that data stored in this instance is associated with or can be converted to; false to retrieve only native data formats.</param>
+        /// <returns>
+        /// An array of the names that represents a list of all formats that are supported by the data stored in this object.
+        /// </returns>
+        public string[] GetFormats(bool autoConvert)
+        {
+            return this.underlyingDataObject.GetFormats(autoConvert);
+        }
+
+        /// <summary>
+        /// Stores the specified data in this instance, using the class of the data for the format.
+        /// </summary>
+        /// <param name="data">The data to store.</param>
+        public void SetData(object data)
+        {
+            this.underlyingDataObject.SetData(data);
+        }
+
+        /// <summary>
+        /// Stores the specified data and its associated class type in this instance.
+        /// </summary>
+        /// <param name="format">A <see cref="T:System.Type"></see> representing the format associated with the data. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="data">The data to store.</param>
+        public void SetData(Type format, object data)
+        {
+            this.underlyingDataObject.SetData(format, data);
+        }
+
+        /// <summary>
+        /// Stores the specified data and its associated format in this instance.
+        /// </summary>
+        /// <param name="format">The format associated with the data. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="data">The data to store.</param>
+        public void SetData(string format, object data)
+        {
+            this.underlyingDataObject.SetData(format, data);
+        }
+
+        /// <summary>
+        /// Stores the specified data and its associated format in this instance, using a Boolean value to specify whether the data can be converted to another format.
+        /// </summary>
+        /// <param name="format">The format associated with the data. See <see cref="T:System.Windows.DataFormats"></see> for predefined formats.</param>
+        /// <param name="autoConvert">true to allow the data to be converted to another format; otherwise, false.</param>
+        /// <param name="data">The data to store.</param>
+        public void SetData(string format, object data, bool autoConvert)
+        {
+            this.underlyingDataObject.SetData(format, data, autoConvert);
+        }
+
+        #endregion
+    }
+}

+ 64 - 172
inabox.wpf/DynamicGrid/DynamicDocumentGrid.cs

@@ -28,6 +28,7 @@ using Syncfusion.Windows.PdfViewer;
 using Border = System.Windows.Controls.Border;
 using Color = System.Windows.Media.Color;
 using Image = System.Windows.Controls.Image;
+using InABox.Wpf;
 
 namespace InABox.DynamicGrid
 {
@@ -52,8 +53,6 @@ namespace InABox.DynamicGrid
                 : Set;
         }
     }
-    
-    public delegate String OnGetWatermark(CoreRow row);
 
     public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyToManyGrid<TDocument, TEntity>
         where TEntity : Entity, IPersistent, IRemotable, new()
@@ -215,6 +214,7 @@ namespace InABox.DynamicGrid
                 Background = new SolidColorBrush(Colors.Transparent),
                 Height = 32,
                 Width = 32,
+                ToolTip = "View Documents",
                 Command = new ActionCommand(ViewDocuments)
             };
             buttons.Children.Add(view);
@@ -227,6 +227,7 @@ namespace InABox.DynamicGrid
                 Background = new SolidColorBrush(Colors.Transparent),
                 Height = 32,
                 Width = 32,
+                ToolTip = "Copy to Clipboard",
                 Command = new ActionCommand(CopyDocuments)
             };
             buttons.Children.Add(copy);
@@ -238,6 +239,7 @@ namespace InABox.DynamicGrid
                 Background = new SolidColorBrush(Colors.Transparent),
                 Height = 32,
                 Width = 32,
+                ToolTip = "Save Documents",
                 Command = new ActionCommand(SaveDocuments)
             };
             buttons.Children.Add(save);
@@ -249,6 +251,7 @@ namespace InABox.DynamicGrid
                 Background = new SolidColorBrush(Colors.Transparent),
                 Height = 32,
                 Width = 32,
+                ToolTip = "Print Documents",
                 Command = new ActionCommand(PrintDocuments)
             };
             buttons.Children.Add(print);
@@ -376,167 +379,53 @@ namespace InABox.DynamicGrid
             options.Add(DynamicGridOption.DragTarget);
         }
         
-        // private bool SaveDocument(CoreRow? row)
-        // {
-        //     var filename = row.Get<TDocument, string>(x => x.DocumentLink.FileName);
-        //     if (Path.GetExtension(filename).ToUpper().Equals(".PDF"))
-        //     {
-        //         var dlg = new SaveFileDialog();
-        //         dlg.Filter = "PDF Files (*.pdf)|*.pdf";
-        //         dlg.FileName = Path.ChangeExtension(filename, ".pdf");
-        //         if (dlg.ShowDialog() == true)
-        //         {
-        //             var imageid = row.Get<TDocument, Guid>(x => x.DocumentLink.ID);
-        //             var data = new Client<Document>().Query(new Filter<Document>(x => x.ID).IsEqualTo(imageid)).Rows.FirstOrDefault().Get<Document, byte[]>(x => x.Data);
-        //             var name = dlg.FileName;
-        //             File.WriteAllBytes(name, data);
-        //
-        //             var gsProcessInfo = new ProcessStartInfo();
-        //             gsProcessInfo.Verb = "open";
-        //             gsProcessInfo.WindowStyle = ProcessWindowStyle.Normal;
-        //             gsProcessInfo.FileName = name;
-        //             gsProcessInfo.UseShellExecute = true;
-        //
-        //             Process.Start(gsProcessInfo);
-        //         }
-        //
-        //     }
-        //     else if (Path.GetExtension(filename).ToUpper().Equals(".PNG") || Path.GetExtension(filename).ToUpper().Equals(".JPG") || Path.GetExtension(filename).ToUpper().Equals(".GIF"))
-        //     {
-        //         var imageid = row.Get<TDocument, Guid>(x => x.DocumentLink.ID);
-        //         if (imageid == Guid.Empty)
-        //             return false;
-        //
-        //         var dlg = new SaveFileDialog();
-        //         dlg.Filter = "Image Files (*.png)|*.png";
-        //         dlg.FileName = filename;
-        //         if (dlg.ShowDialog() == true)
-        //         {
-        //             var bmp = LoadBitmapFromDatabase(imageid);
-        //             bmp?.Save(dlg.FileName);
-        //         }
-        //     }
-        //
-        //     return false;
-        // }
-        //
-        // private Bitmap LoadBitmapFromDatabase(Guid imageid)
-        // {
-        //     if (imageid == Guid.Empty)
-        //         return null;
-        //     Bitmap result = null;
-        //     var image = new Client<Document>().Query(
-        //         new Filter<Document>(x => x.ID).IsEqualTo(imageid),
-        //         new Columns<Document>(x => x.ID, x => x.Data)
-        //     ).Rows.FirstOrDefault();
-        //     if (image != null)
-        //     {
-        //         var ms = new MemoryStream(image.Get<Document, byte[]>(x => x.Data));
-        //         result = new Bitmap(ms);
-        //     }
-        //
-        //     return result;
-        // }
-
-        // private BitmapImage? DiskImage(CoreRow? arg)
-        // {
-        //     return Wpf.Resources.disk.AsBitmapImage();
-        // }
 
         public override int Order()
         {
             return int.MaxValue;
         }
 
-        // private BitmapImage SupercededImage(CoreRow? row)
-        // {
-        //     if (row == null)
-        //         return Wpf.Resources.tick.AsBitmapImage();
-        //     if (row.Get<TDocument, DateTime>(x => x.Superceded) != DateTime.MinValue)
-        //         return Wpf.Resources.warning.AsBitmapImage();
-        //     return Wpf.Resources.tick.AsBitmapImage();
-        // }
-        //
-        // private bool SupercedeDocument(CoreRow? row)
-        // {
-        //     if (!ReadOnly)
-        //     {
-        //         var id = row.Get<TDocument, Guid>(x => x.ID);
-        //         var document = WorkingList.FirstOrDefault(x => x.ID.Equals(id));
-        //         if (document != null)
-        //             document.Superceded = document.Superceded == DateTime.MinValue ? DateTime.Now : DateTime.MinValue;
-        //         return true;
-        //     }
-        //     else
-        //     {
-        //         return false;
-        //     }
-        // }
-        //
-        // private BitmapImage DocumentImage(CoreRow? arg)
-        // {
-        //     return Wpf.Resources.view.AsBitmapImage();
-        // }
-        //
-        // private bool ViewDocument(CoreRow? row)
-        // {
-        //     var filename = row.Get<TDocument, string>(x => x.DocumentLink.FileName);
-        //     if (Path.GetExtension(filename).ToUpper().Equals(".PDF"))
-        //     {
-        //         var viewer = new DocumentEditor(row.ToObject<TDocument>());
-        //         viewer.Watermark = OnGetWaterMark?.Invoke(row);
-        //         //viewer.PrintAllowed = true;
-        //         viewer.SaveAllowed = true;
-        //         viewer.ShowDialog();
-        //     }
-        //     else
-        //     {
-        //         var id = row.Get<TDocument, Guid>(x => x.DocumentLink.ID);
-        //         var docrow = new Client<Document>().Query(new Filter<Document>(x => x.ID).IsEqualTo(id)).Rows.FirstOrDefault();
-        //         if (docrow != null)
-        //         {
-        //             var tmpfile = Path.ChangeExtension(Path.GetTempFileName(), Path.GetExtension(filename));
-        //             File.WriteAllBytes(tmpfile, docrow.Get<Document, byte[]>(x => x.Data));
-        //             var gsProcessInfo = new ProcessStartInfo();
-        //             gsProcessInfo.Verb = "open";
-        //             gsProcessInfo.WindowStyle = ProcessWindowStyle.Normal;
-        //             gsProcessInfo.FileName = tmpfile;
-        //             gsProcessInfo.UseShellExecute = true;
-        //
-        //             Process.Start(gsProcessInfo);
-        //         }
-        //         else
-        //         {
-        //             MessageBox.Show(string.Format("Unable to retrieve {0}!", filename));
-        //         }
-        //     }
-        //
-        //     //Document doc = new Client<Document>().Load(new Filter<Document>(x => x.ID).IsEqualTo(id)).FirstOrDefault();
-        //     //if (doc != null)
-        //     //{
-        //     //    if (System.IO.Path.GetExtension(doc.FileName).ToUpper().Equals(".PDF"))
-        //     //    {
-        //     //        PDFViewer viewer = new PDFViewer(doc);
-        //     //        viewer.ShowDialog();
-        //     //    }
-        //     //    else
-        //     //    {
-        //     //        String filename = System.IO.Path.ChangeExtension(System.IO.Path.GetTempFileName(), System.IO.Path.GetExtension(doc.FileName));
-        //     //        System.IO.File.WriteAllBytes(filename, doc.Data);
-        //     //        ProcessStartInfo gsProcessInfo = new ProcessStartInfo();
-        //     //        gsProcessInfo.Verb = "open";
-        //     //        gsProcessInfo.WindowStyle = ProcessWindowStyle.Normal;
-        //     //        gsProcessInfo.UseShellExecute = true;
-        //     //        gsProcessInfo.FileName = filename;
-        //     //        Process.Start(gsProcessInfo);
-        //     //    }
-        //     //}
-        //     //else
-        //     //    MessageBox.Show("Document does nto exist!");
-        //     return false;
-        // }
+        protected override void HandleDragOver(object sender, DragEventArgs e)
+        {
+            if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent("FileGroupDescriptor"))
+            {
+                e.Effects = DragDropEffects.Copy;
+            }
+            else
+            {
+                e.Effects = DragDropEffects.None;
+            }
+            e.Handled = true;
+        }
 
-        public event OnGetWatermark OnGetWaterMark;
+        protected override void HandleDragDrop(object sender, DragEventArgs e)
+        {
+            var result = DocumentUtils.HandleFileDrop(e);
+            if(result is not null)
+            {
+                var docs = new List<Document>();
+                foreach (var (filename, stream) in result)
+                {
+                    var doc = new Document();
+                    doc.FileName = Path.GetFileName(filename).ToLower();
+                    if (stream is null)
+                    {
+                        doc.Data = File.ReadAllBytes(filename);
+                        doc.TimeStamp = new FileInfo(filename).LastWriteTime;
+                    }
+                    else
+                    {
+                        using var memoryStream = new MemoryStream();
+                        stream.CopyTo(memoryStream);
+                        doc.Data = memoryStream.ToArray();
+                        doc.TimeStamp = DateTime.Now;
+                    }
+                    doc.CRC = CoreUtils.CalculateCRC(doc.Data);
+                    docs.Add(doc);
+                }
+                AddDocuments(docs);
+            }
+        }
 
         protected override void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
         {
@@ -579,6 +468,24 @@ namespace InABox.DynamicGrid
             }
         }
 
+        private void AddDocuments(IList<Document> documents)
+        {
+            if (documents.Any())
+            {
+                new Client<Document>().Save(documents, "Initial Upload");
+                foreach (var doc in documents)
+                {
+                    var newitem = CreateItem();
+                    var prop = GetOtherLink(newitem);
+                    prop.ID = doc.ID;
+                    prop.Synchronise(doc);
+                    SaveItem(newitem);
+                }
+                DoChanged();
+                Refresh(false, true);
+            }
+        }
+
         protected override void DoAdd(bool OpenEditorOnDirectEdit = false)
         {
             var dlg = new OpenFileDialog();
@@ -598,23 +505,8 @@ namespace InABox.DynamicGrid
                         doc.CRC = CoreUtils.CalculateCRC(doc.Data);
                         docs.Add(doc);
                     }
-
-                    if (docs.Any())
-                    {
-                        new Client<Document>().Save(docs, "Initial Upload");
-                        foreach (var doc in docs)
-                        {
-                            var newitem = CreateItem();
-                            var prop = (IEntityLink)otherproperty.GetValue(newitem);
-                            prop.ID = doc.ID;
-                            prop.Synchronise(doc);
-                            SaveItem(newitem);
-                        }
-                        DoChanged();
-                    }
+                    AddDocuments(docs);
                 }
-
-                Refresh(false, true);
             }
         }
     }

+ 15 - 0
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -3462,6 +3462,12 @@ namespace InABox.DynamicGrid
 
         private static string DragFormat => typeof(DynamicGridDragFormat).FullName ?? "";
 
+        /// <summary>
+        /// Handle a number of rows from a different <see cref="DynamicGrid{T}"/> being dragged into this one.
+        /// </summary>
+        /// <param name="entity">The type of entity that that the rows of <paramref name="table"/> represent.</param>
+        /// <param name="table">The data being dragged.</param>
+        /// <param name="e"></param>
         protected virtual void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
         {
             Logger.Send(LogType.Information,"","OnDragEnd");
@@ -3516,6 +3522,15 @@ namespace InABox.DynamicGrid
             }
         }
 
+        /// <summary>
+        /// Handle all types of items being dragged onto this grid that aren't handled by <see cref="OnDragEnd(Type, CoreTable, DragEventArgs)"/>,
+        /// i.e., data which is not a <see cref="CoreTable"/> from another <see cref="DynamicGrid{T}"/>
+        /// </summary>
+        /// <remarks>
+        /// Can be used to handle files, for example.
+        /// </remarks>
+        /// <param name="sender"></param>
+        /// <param name="e"></param>
         protected virtual void HandleDragDrop(object sender, DragEventArgs e)
         {
         }