Virtualizing a Data Object

This is going to be a long post, so get yourself some coffee or your development beverage of choice before you start. This is something that was brought up during the PDC, and so I'm following up with one of the solutions suggested.

The problem is as follows: how do I write my application such that the data that the user copies to the clipboard isn't generated until the user actually pastes it elsewhere? This is a fairly common scenario for applications that copy their own data to the clipboard - multiple formats can be produced, but only one is consumed by the app where the paste occurs, so there is little point in generating the representation for the other formats.

The most flexible and complex way of doing this is implementing the IDataObject interface found in System.Runtime.InteropServices.ComTypes. When you create a DataObject with an implementation of this interface in the constructor, the DataObject will essentially become a wrapper for it, forwarding all calls.

An easier way of getting this done is to use a custom Stream subclass that will generate data when required. When the time comes to marshal the data to the pasting app, the stream can generate its content as it's read.

Let's build a little app to play with this step-by-step, shall we?

First, make sure you have the September CTP installed on your computer. Next, create a file named virtualized-do.cs, and let's get down to the code!

// Build with the following command-line from the build environment console.
// csc.exe /r:"%windir%\Microsoft.Net\Windows\v6.0.5070\PresentationCore.dll" /r:"%windir%\Microsoft.Net\Windows\v6.0.5070\WindowsBase.dll" /r:"%windir%\Microsoft.Net\Windows\v6.0.5070\PresentationFramework.dll" /r:"%windir%\Microsoft.Net\Windows\v6.0.5070\UIAutomationProvider.dll" virtualized-do.cs

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;

namespace Sample {
class EntryPoint {

[STAThread]
public static void Main() {
Window window = new Window();
Button button = new Button();

button.Content = "Click me to place data object in Clipboard";
window.Content = button;
new Application().Run(window);
}
}
}

So far, this is a plain vanilla Windows Presentation Foundation app. Now, let's write a Stream subclass that only generates its data when needed, in the same namespace block.

class MyVirtualizingStream: Stream {
public override bool CanRead { get { return true; } }
public override bool CanWrite { get { return false; } }
public override bool CanSeek { get { return true; } }
public override bool CanTimeout { get { return false; } }

public override long Length
{ get { EnsureValid(); return inner.Length; } }

public override long Position
{ get { return (valid)? inner.Length : 0; }
set { EnsureValid(); inner.Position = value; } }

public override int Read(byte[] buffer, int offset, int length)
{ EnsureValid(); return inner.Read(buffer, offset, length); }

public override long Seek(long offset, SeekOrigin origin)
{ EnsureValid(); return inner.Seek(offset, origin); }

public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override void Flush() { }

  private Stream inner;
private bool valid;
private void EnsureValid() {
if (valid) return;

    // Not very smart: just delaying the (at this point) inevitable.
// Here you would pull out to get real data, by looking at some
// internal structure or raising an event.

    inner = new MemoryStream();

    // Note: we could use a StreamWriter, but it will write the BOM, which other apps do not expect.
// We'll handle this ourselves then. Also, note the trailing null character.
byte[] encodedString;
encodedString = System.Text.Encoding.Unicode.GetBytes("Hi, mom!\0");
inner.Write(encodedString, 0, encodedString.Length);
inner.Position = 0;

    Console.WriteLine("My virtualizing stream created content.");

    valid = true;
}
}

As you can see, this is a very bare-bones implementation, that delegates most calls to an internal stream, which ends up being a MemoryStream populated with hard-coded content. Your internal logic to pull in data from a different format or from the copy-time instant in your app would go here.

Now, we can use this stream when copying to the clipboard. Let's add some code to the main function of our app.

public static void Main() {
DataObject dataObject = new DataObject();
Window window = new Window();
Button button = new Button();

  button.Content = "Click me to place data object in Clipboard";
button.Click += delegate {
dataObject = new DataObject();
dataObject.SetData(DataFormats.UnicodeText, new MyVirtualizingStream());

    // Note that we do *not* auto-flush, which means the clipboard
// only has a reference to our data object. If the app
// terminates, then the clipboard will be empty.
Clipboard.SetDataObject(dataObject);
Console.WriteLine("Data placed on clipboard.");
};

  window.Content = button;
new Application().Run(window);
}

At last, something interesting to see. If you run the application and click the button, you will notice that the data has not been generated yet. If you close the application and try to paste, nothing will happen. Now run the application again, click the button, then paste into notepad. The text will appear, as well as a line in the console window stating that the data was generated on demand - exactly what we wanted.

Now, what's with the auto-flush thing? When placing data on the clipboard, we can choose to "flush it", that is, to put all the content instead of just a reference to our data object. This means the data is still available when our app goes away, but it also means that all data formats are pulled from our app. Baad. So, we don't flush, but then we need to ensure that we *do* place our data on the clipboard at some point, which is what users typically expect.

...
window.Content = button;
window.Closed += delegate {
// If we are on the clipboard, then we should flush our data,
// so the user can still paste it even when we're gone. Note
// that another common thing to do is to just ask the user
// if he wants this when the data is very large - Microsoft
// Office does something like this.
if (Clipboard.IsCurrent(dataObject)) {
// OK, we will flush the data to the clipboard now.
Clipboard.SetDataObject(dataObject, true);
Console.WriteLine("Data flushed to the clipboard.");
} else {
Console.WriteLine("Data not current in clipboard - nothing to do.");
}
};

new Application().Run(window);
...

And there you have it. Not too difficult, and quite efficient when you want to expose your data as some properietary format and text and CSV, for example.

This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at https://www.microsoft.com/info/cpyright.htm.