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 http://www.microsoft.com/info/cpyright.htm.


Comments (4)

  1. Eyboy says:

    Hi there. Interesting post. I’m trying to do something similar with avalon. And currently, I’m trying to do this using the other method you said at the beginning of your article. Anyway, I have a couple of questions:

    1. Using the "Stream" approach, does this mean that you have to convert the data formats into a stream for it to be placed on the clipboard? I don’t have much experience programming with streams so I want to ask, is it simple to write a code that converts, say a custom object, to stream?

    2. You said another way of doing this that requires you to implement the IDataObject interface from System.Runtime.InteropServices.ComTypes. Is this interface the same as System.Windows.IDataObject?

    Thanks. =)

  2. Eyboy:

    1. Data is transfered between applications in a given format / medium combination. For example, text is typically transferred as a chunk of global memory; bitmaps may be transferred as handle to bitmaps, and so on. For formats that can be transfered as global memory, the WPF DataObject will suck out the contents of a Stream for you – the trick is that you can generate the content for the stream "on demand."

    Now, converting a custom object to a stream shouldn’t be too hard – in fact, you can probably use a BinaryFormatter to do most of the work for you [http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemRuntimeSerializationFormattersBinaryBinaryFormatterClassTopic.asp?frame=true].

    Data transfer is a big topic – let me know if you’d like me to go a bit deeper into any of it, I know I’m being kind of brief in this comment.

    2. No, System.Windows.IDataObject is an interface that is more .NET-friendly than the one in S.R.I.C. The latter is the "official" OLE interface, with the right binary-compatible members, while the former is a useful abstraction over "things that behave like data objects", without OLE plumbing that’s unnecessary in the managed world.

  3. Eyboy says:

    Hi again marcelo. Thanks for your reply. Anyway, I have a couple more questions if you don’t mind. You see, I have been able to do ‘delayed rendering’ by using the other method you suggested, i.e. creating a custom class that implements IDataObject (i used the one from System.Windows though because this is easier to use) and internally using a System.Windows.DataObject class. However, I’m encountering a couple of problems:

    1. Say I do the following:

    MyDataObject _mdo = new MyDataObject();

    Clipboard.SetDataObject(_mdo);

    IDataObject _ido = Clipboard.GetDataObject();

    After executing the code above, the variable _ido is of type System.Windows.DataObject instead of MyDataObject. It’s a different dataobject! I cannot even cast it to MyDataObject. Moreover, when i do _ido.GetData("DataObject"), it returns an object of type MemoryStream. I was wondering if this is my original data object but I don’t know how to convert it. Can you help me out? Thanks.

    2. I’m not sure if this is a bug in .Net or what but it seems to me that whenever I place a System.Windows.DataObject in the clipboard and paste it in another application, say MS Word, an error appears saying "Invalid FORMATETC structure". It does not happen in notepad though. What does this mean?

    Thanks again for your help.

    -Eyboy

  4. In the past (Virtualizing a Data Object), I’ve talked about virtualizing a data object on the clipboard…

Skip to main content