After some research, I was able to figure out how to do this without using the IDataObject COM interface (with all its FORMATETC gunk). I thought this might be of interest to others in the same predicament, so I wrote my decision. If this can be done more cleverly, I am all eyes / ears!
The System.Windows.Forms.DataObject class has this constructor:
public DataObject(string format, object data)
I called it this way:
string expensive = GenerateStringVerySlowly(); var dataObject = new DataObject( DataFormats.UnicodeText, expensive); DoDragDrop(dataObject, DragDropEffects.Copy);
The above code will put the string data in HGLOBAL during the copy operation. However, you can also call the constructor as follows:
string expensive = GenerateStringVerySlowly(); var dataObject = new DataObject( DataFormats.UnicodeText, new MemoryStream(Encoding.Unicode.GetBytes(expensive))); DoDragDrop(dataObject, DragDropEffects.Copy);
Instead of copying data through HGLOBAL this later call has the nice effect of copying data through (COM) IStream . There seems to be some magic in the .NET interop environment that handles the mapping between COM IStream and the .NET System.IO.Stream .
Now I needed to write a class that delayed the creation of the stream until the very last minute ( Lazy object pattern ), when the return target starts calling Length , Read , etc. It looks like this: (details edited for brevity)
public class DeferredStream : Stream { private Func<string> generator; private Stream stm; public DeferredStream(Func<string> expensiveGenerator) { this.generator = expensiveGenerator; } private Stream EnsureStream() { if (stm == null) stm = new MemoryStream(Encoding.Unicode.GetBytes(generator())); return stm; } public override long Length { get { return EnsureStream().Length; } } public override long Position { get { return EnsureStream().Position; } set { EnsureStream().Position = value; } } public override int Read(byte[] buffer, int offset, int count) { return EnsureStream().Read(buffer, offset, count); }
Please note that expensive data is only generated when the EnsureStream method EnsureStream called for the first time. This does not happen until the return target begins to IStream data in IStream . Finally, I changed the call code to:
var dataObject = new DataObject( DataFormats.UnicodeText, new DeferredStream(GenerateStringVerySlowly)); DoDragDrop(dataObject, DragDropEffects.Copy);
That was exactly what I needed to do the job. However, I rely on good target behavior. An incorrect drop in goals that would implicitly cause, say, the Read method, say, will lead to an expensive operation occurring sooner.