5

I am successfully using VirtualFileDataObject code from Delay's blog, but i want to avoid streaming the entire file into memory.

I found this previously answered question on Stack Overflow Drag and Drop large virtual files from c# to Windows Explorer The question was answered by matthieu, by changing the signature of the SetData method.

Here is my problem, after changing the signature of the SetData method, other places that call it are still looking for the old signature.

Here is the original SetData;

   public void SetData(short dataFormat, int index, Action<Stream> streamData)
    {
        _dataObjects.Add(
            new DataObject
            {
                FORMATETC = new FORMATETC
                {
                    cfFormat = dataFormat,
                    ptd = IntPtr.Zero,
                    dwAspect = DVASPECT.DVASPECT_CONTENT,
                    lindex = index,
                    tymed = TYMED.TYMED_ISTREAM
                },
                GetData = () =>
                {
                    // Create IStream for data
                    var ptr = IntPtr.Zero;
                    var iStream = NativeMethods.CreateStreamOnHGlobal(IntPtr.Zero, true);
                    if (streamData != null)
                    {
                        // Wrap in a .NET-friendly Stream and call provided code to fill it
                        using (var stream = new IStreamWrapper(iStream))
                        {
                            streamData(stream);
                        }
                    }
                    // Return an IntPtr for the IStream
                    ptr = Marshal.GetComInterfaceForObject(iStream, typeof(IStream));
                    Marshal.ReleaseComObject(iStream);
                    return new Tuple<IntPtr, int>(ptr, NativeMethods.S_OK);
                },
            });
    }

matthieu suggested to change it to;

public void SetData(short dataFormat, int index, Stream stream)
{
  ...
  var iStream = new StreamWrapper(stream);
  ...
  // Ensure the following line is commented out:
  //Marshal.ReleaseComObject(iStream);
  return new Tuple<IntPtr, int>(ptr, NativeMethods.S_OK);
 ...
}

After I make these changes the following call will not work; ( and this is where i need help) How do i fix this call;

            foreach (var fileDescriptor in fileDescriptors)
        {
            **SetData(FILECONTENTS, index, fileDescriptor.StreamContents);**
            index++;
        }

Basically changing "Action streamData" To "Stream stream" is causing my problems. I am not sure on how to call it after the changes are made.

All this code comes from Delays VirtualFileDataObject. I don't know if i should post it on here or not. But if you follow the link above it will take you to the blog so you can view it.

I am so close, just can't figure this last step out, thanks for taking a look

Community
  • 1
  • 1
teknorj
  • 51
  • 4

1 Answers1

4

I've had exactly the same problem. Here is what I did to fix this issue (which as you say has not been fully addressed in the other answer)

1) Modify FileDescriptor's StreamContents property from this:

public Action<Stream> StreamContents { get; set; }

to this:

public Func<Stream> StreamContents { get; set; }

(instead of passing a Stream the client can write, we'll expect a Stream we can read from, which is exactly how Explorer works and what it expects)

2) Modify the SetData method overload from this:

public void SetData(short dataFormat, int index, Action<Stream> streamData)

to this:

public void SetData(short dataFormat, int index, Func<Stream> streamData)

3) change SetData code's GetData lambda to this:

GetData = () =>
{
    ManagedIStream istream = null;
    if (streamData != null)
    {
        Stream stream = streamData();
        if (stream != null)
        {
            istream = new ManagedIStream(stream);
        }
    }

    IntPtr ptr = istream != null ? Marshal.GetComInterfaceForObject(istream, typeof(IStream)) : IntPtr.Zero;
    return new Tuple<IntPtr, int>(ptr, NativeMethods.S_OK);
},

4) add this ManagedIStream class to the code (you can also delete the IStreamWrapper class completely)

private class ManagedIStream : IStream
{
    private Stream _stream;

    public ManagedIStream(Stream stream)
    {
        _stream = stream;
    }

    public void Clone(out IStream ppstm)
    {
        throw new NotImplementedException();
    }

    public void Commit(int grfCommitFlags)
    {
        throw new NotImplementedException();
    }

    public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
    {
        throw new NotImplementedException();
    }

    public void LockRegion(long libOffset, long cb, int dwLockType)
    {
        throw new NotImplementedException();
    }

    public void Read(byte[] pv, int cb, IntPtr pcbRead)
    {
        int read = _stream.Read(pv, 0, cb);
        if (pcbRead != IntPtr.Zero)
        {
            Marshal.WriteInt32(pcbRead, read);
        }
    }

    public void Revert()
    {
        throw new NotImplementedException();
    }

    public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
    {
        long newPos = _stream.Seek(dlibMove, (SeekOrigin)dwOrigin);
        if (plibNewPosition != IntPtr.Zero)
        {
            Marshal.WriteInt64(plibNewPosition, newPos);
        }
    }

    public void SetSize(long libNewSize)
    {
        _stream.SetLength(libNewSize);
    }

    public void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
    {
        const int STGTY_STREAM = 2;
        pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
        pstatstg.type = STGTY_STREAM;
        pstatstg.cbSize = _stream.Length;
        pstatstg.grfMode = 0;

        if (_stream.CanRead && _stream.CanWrite)
        {
            const int STGM_READWRITE = 0x00000002;
            pstatstg.grfMode |= STGM_READWRITE;
            return;
        }

        if (_stream.CanRead)
        {
            const int STGM_READ = 0x00000000;
            pstatstg.grfMode |= STGM_READ;
            return;
        }

        if (_stream.CanWrite)
        {
            const int STGM_WRITE = 0x00000001;
            pstatstg.grfMode |= STGM_WRITE;
            return;
        }

        throw new IOException();
    }

    public void UnlockRegion(long libOffset, long cb, int dwLockType)
    {
        throw new NotImplementedException();
    }

    public void Write(byte[] pv, int cb, IntPtr pcbWritten)
    {
        _stream.Write(pv, 0, cb);
        if (pcbWritten != IntPtr.Zero)
        {
            Marshal.WriteInt32(pcbWritten, cb);
        }
    }
}

That's it. Now you can use the code like this (using the same sample as in the original article available here: http://dlaa.me/blog/post/9913083):

new VirtualFileDataObject.FileDescriptor
{
    Name = "Alphabet.txt",
    Length = 26,
    ChangeTimeUtc = DateTime.Now.AddDays(-1),
    StreamContents = () =>
    {
        var contents = Enumerable.Range('a', 26).Select(i => (byte)i).ToArray();
        MemoryStream ms = new MemoryStream(contents); // don't dispose/using here, it would be too early
        return ms;
    }
};
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • @DavidRefaeli - Dispose should be called when the caller on the other end (whoever that be) closes the stream. – Simon Mourier Feb 13 '18 at 07:14
  • Hmmm yes, but how? Can you maybe show an example? Where do you call ms.Close() and how do you pass it outside? In my case I'm using an actual file I create in a temp location ( return new FileStream(sanitizedFile, FileMode.Open, FileAccess.Read); ) And it keeps the file handle in the temp location open so I can't delete it... – Maverick Meerkat Feb 13 '18 at 08:48
  • @DavidRefaeli - you don't close it. The guy that will do the copy will close the stream (undercover, its an unmanaged stream). As long as the clipboard data stays, the stream will stay open. That's why I used a MemoryStream. If you do have a physical file, don't use that method, use standard file clipboard formats. – Simon Mourier Feb 13 '18 at 09:31
  • can you please check my question: https://stackoverflow.com/questions/48766727/dragdrop-file-from-wpf-to-explorer-with-processing-the-file-before-drop – Maverick Meerkat Feb 13 '18 at 12:58
  • I tried your solution, it works great with the explorer, but draging into an Outlook email does not work. First I needed to add an implementation for IStream CopyTo funtion, but even after that Outlook gives an non helping message "Operation failed". Any idea how to get it working also in that case ? – Eric Pitz Aug 10 '20 at 14:08