Virtualizing a Data Object (redux)

In the past (Virtualizing a Data Object), I've talked about virtualizing a data object on the clipboard or in a drag-drop operation by using a Stream subclass. I have recently received some questions about this and I wanted to clarify this a bit (and throw in some nice goodies while I'm at it).

To begin with, do not be misled by CopyingEvent, PastingEvent and SettingDataEvent. These are not meant to be used for virtualization; rather, they are meant to be used as a customizing protocol for content editors. In particular, you can tweak the behavior of the text editor - I'll blog more about this in the future.

One of the most flexible ways of customizing a data object is by implementing IDataObject. I have written a sample application showing the bare minimum needed for this.

On the Beta bits, create a new application in Visual Studio, and use this for the Window1.xaml file.

<Window x:Class="MyVirtualDataObject.Window1"
xmlns="
https://schemas.microsoft.com/winfx/2006/xaml/presentation "
xmlns:x="
https://schemas.microsoft.com/winfx/2006/xaml "
Title="MyVirtualDataObject" Height="300" Width="300"
>
<DockPanel>
<CheckBox Name="FlushBox"
DockPanel.Dock="Top">Flush on copying</CheckBox>
<Button Name="FakeCopyButton"
DockPanel.Dock="Top"
Click="FakeCopyButtonClick">Fake Copy</Button>
<RichTextBox Name="LogBox" AcceptsReturn="True" />
</DockPanel>
</Window>

As you can see, nothing very sophisticated - a check box to control flushing-on-copying, a button to do a 'fake' (virtualized) copy, and a rich text box to log system activity.

Next, in Window1.xaml.cs, add this method to the Window1 class.

public void FakeCopyButtonClick(object sender, EventArgs e)
{
LogBox.AppendText("Fake Copy button clicked.\r\n");

VirtualDataObject myDataObject = new VirtualDataObject();
myDataObject.FakeData = "Some text";

  Clipboard.SetDataObject(myDataObject, FlushBox.IsChecked.Value);
}

So, what is this mysterious VirtualDataObject class? This is our custom data object, which we'll implement now. Let's start with the class declaration.

public class VirtualDataObject : IDataObject
{
public string FakeData;

  // More stuff comes here...
}

Now, let's add the implementation for getting data and supported formats. I'm adding support for some simple formats, but this code is mostly a placehold - you would normally write something considerably more sophisticated if you wanted to support multiple representations like this.

  #region IDataObject Members

  public object GetData(string format, bool autoConvert)
{
System.Diagnostics.Trace.WriteLine("GetData " + format + " " + autoConvert);

    if (format == DataFormats.CommaSeparatedValue)
{
return "1,TheData\r\n2," + FakeData;
}
else if (format == DataFormats.Rtf)
{
return @"{\rtf1\ansi\ansicpg1252\deff0\deflang1033" +
@"{\fonttbl{\f0\fswiss\fcharset0 Consolas;}}" +
@"\pard\f0\fs40 " + FakeData + "}";
}
else if (format == DataFormats.Text || format == DataFormats.UnicodeText)
{
return FakeData;
}
else if (format == DataFormats.Xaml)
{
return "<Span xmlns='https://schemas.microsoft.com/winfx/2006/xaml/presentation'>" +
"<Run FontWeight='Bold'>" + FakeData + "</Run></Span>";
}
else
{
return null;
}
}

  public object GetData(Type format)
{
if (format == typeof(string))
{
return GetData(DataFormats.UnicodeText, false);
}
else
{
return null;
}
}

  public object GetData(string format)
{
return GetData(format, false);
}

  public bool GetDataPresent(string format, bool autoConvert)
{
System.Diagnostics.Trace.WriteLine(
"GetDataPresent " + format + " " + autoConvert);
if (format == DataFormats.CommaSeparatedValue ||
format == DataFormats.Rtf ||
format == DataFormats.Text ||
format == DataFormats.UnicodeText ||
format == DataFormats.Xaml)
{
return true;
}
else
{
return false;
}
}

  public bool GetDataPresent(Type format)
{
if (format == typeof(string))
{
return GetDataPresent(DataFormats.UnicodeText, true);
}
else
{
return false;
}
}

  public bool GetDataPresent(string format)
{
return GetDataPresent(format, false);
}

  public string[] GetFormats(bool autoConvert)
{
System.Diagnostics.Trace.WriteLine(
"GetFormats " + autoConvert);
return new string[] {
DataFormats.CommaSeparatedValue,
DataFormats.Rtf,
DataFormats.Text,
DataFormats.UnicodeText,
DataFormats.Xaml
};
}

  public string[] GetFormats()
{
return GetFormats(false);
}

  // A few remaining things go here...

Finally, let's add some code to make sure we comply with the interface, even if we're never going to set data on this object through these methods.

  public void SetData(string format, object data, bool autoConvert)
{
throw new Exception("The method or operation is not implemented.");
}

  public void SetData(Type format, object data)
{
throw new Exception("The method or operation is not implemented.");
}

  public void SetData(string format, object data)
{
throw new Exception("The method or operation is not implemented.");
}

  public void SetData(object data)
{
throw new Exception("The method or operation is not implemented.");
}

  #endregion

You can now run the application, click the button, and then paste in Notepad or Wordpad. You'll see that we don't actually get a call to GetData until the moment in which we do the paste in Notepad (the traces appear in Debug | Window | Output in Visual Studio).

In this sample, I'm just using a fake data field, but you can imagine that we could do a lot of processing in the GetData method to do the actual format convertion and/or generate the data we need.

If you select the checkbox flushing option, then you'll see that when you click the button, everything happens at once. This is because the data object will be available even after our application is closed, so the system is 'sucking out' all the data it can from the data object.

Hooking up a trace listener
You'll note that we're using the tracing from System.Diagnostics rather than writing directly to the log box. This is something that I do just to decouple things a bit. To hook the tracing back together, here's a reusable trace listener class.

public class TextBoxBaseTraceListener : System.Diagnostics.TraceListener
{
public TextBoxBaseTraceListener(
System.Windows.Controls.Primitives.TextBoxBase control)
{
if (control == null)
{
throw new ArgumentNullException("control");
}
this.control = control;
}

  private System.Windows.Controls.Primitives.TextBoxBase control;

  public override void Write(string message)
{
control.AppendText(message);
}

  public override void WriteLine(string message)
{
control.AppendText(message);
control.AppendText(Environment.NewLine);
}
}

To hook this up, add a bit of code to the window constructor so it looks like this. Note that in a real app, you would want to unhook this at some point!

public Window1()
{
InitializeComponent();

  System.Diagnostics.Trace.Listeners.Add(
new TextBoxBaseTraceListener(LogBox));
}

In some future post, I'll also look at what it takes to make the trace listener thread-safe (right now, tracing from a background thread could cause problems). Stay safe!

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.