Capture object state with visualizers





5.00/5 (8 votes)
Visualizer to capture object state.
Introduction
A debugger visualizer provides a debug time interface to present a variable or object in a meaningful way. Visualizers are represented in the Visual Studio debugger by a magnifying glass icon on a DataTip, in a debugger variables window or in the QuickWatch dialog box. Clicking the magnifying glass lists down the available set of debugger visualizers for the variable.
This article demonstrates how to capture the state of a .NET object during a debugging session using Visual Studio debugger visualizer classes. The main objective is to persist a data-contract object. I have also tried to provide a basic implementation to save/load just any .NET object into/from one of the following formats: XML, binary, or SOAP (the binary and SOAP formats are WIP). Although these might be enough for simple objects, we may need to modify the basic implementation on a case by case basis to address more complex scenarios. Once the state is saved into a file it can be used to hydrate (load) an object of the same type at a later point of time.
Background
The Visualizer architecture is neatly explained in the MSDN article: http://msdn.microsoft.com/en-us/library/zayyhzts.aspx.
A visualizer consists of two major components:
- Debuggee side
- Debugger side
Debuggee side
- This is where the object you need to visualize exists.
- The
VisualizerObjectSource
class handles the operations at the debuggee side. - The
VisualizerObjectSource
class needs to be overridden if you want to influence how the target object is transferred to/from the debugger side.
Debugger side:
- This is where you can have your object displayed in a user interface
- Runs within the Visual Studio debugger.
- The
DebuggerSideVisualizer
class handles the operations at the debugger side. - The
Show()
method in theDebuggerSideVisualizer
class needs to be overridden so that you could display a UI for the visualizer. - The Debugger side can send an updated object back to the Debuggee side which could then be used to update the object that is being debugged.
Using the code
For simplicity I will explain just the DataContract visualization part, the rest of the implementation can be understood on the same lines.
Debugge Side
Here is how the debuggee side classes will look like:
If the object you are visualizing has a Serializable
attribute on it then you can rely on the default implementation provided by the Visualizer architecture to transfer the object back and forth at the Debuggee side. Unfortunately that is not the case with a DataContract
object, so we need to override a couple of methods in the VisualizerObjectSource
class.
- At the debuggee side, the
DataContractVisualizerObjectSource
class extends theVisualizerObjectSource
(see class diagram above) to provide custom implementations of theGetData()
andCreateReplacementObject()
methods. - The
GetData()
override uses DataContract serialization and writes it into the “outgoingData
” Stream variable so that it can be received at the Debugger side. CreateReplacementObject()
override method uses the incoming stream data and deserializes it back into the debugged object.
The code snippet below lists the the Debuggee side class DataContractVisualizerObjectSource
.
/// <summary>
/// Extends the VisualizerObjectSource class, uses DataContract serialization to transport a
/// DataContract object between Debuggee and Debugger processes.
/// </summary>
public class DataContractVisualizerObjectSource : VisualizerObjectSource
{
public DataContractVisualizerObjectSource()
{
serializer = new DataContractSerialization();
}
/// <summary>
/// The serializer used by this class.
/// </summary>
private SerializationBase serializer;
/// <summary>
/// Gets data from the specified object and serializes it into the outgoing data stream.
/// </summary>
/// <param name="target">Object being visualized.</param>
/// <param name="outgoingData">Outgoing data stream.</param>
public override void GetData(object target, Stream outgoingData)
{
if (target == null)
return;
var writer = new StreamWriter(outgoingData);
writer.WriteLine(target.GetType().AssemblyQualifiedName);
writer.WriteLine(serializer.Serialize(target));
writer.Flush();
}
/// <summary>
/// Reads an incoming data stream from the debugger side and uses
/// the data to construct a replacement object for the target object.
/// This method is called when ReplaceData or ReplaceObject is called on the debugger side.
/// </summary>
/// <param name="target">Object being visualized.</param>
/// <param name="incomingData">Incoming data stream.</param>
/// <returns>An object, with contents constructed from the incoming data stream,
/// that can replace the target object. This method does not actually replace target
/// but rather provides a replacement object for the debugger
/// to do the actual replacement.</returns>
public override object CreateReplacementObject(object target, Stream incomingData)
{
StreamReader streamReader = new StreamReader(incomingData);
string targetObjectType = streamReader.ReadLine();
return (serializer.Deserialize(Type.GetType(targetObjectType), streamReader.ReadToEnd()));
}
}
Debugger Side
At the Debugger side the Show()
method of the DebuggerSideVisualizer
class needs to be overridden to receive the object sent from the debuggee side and display it in a UI. A few points to note in the Debugger side implementation are:
- The UI component works with the abstract class
DebuggerSideVisualizer
which derives fromDialogDebuggerVisualizer
. - The
DebuggerSideVisualizer
class outlines a set of properties and methods each debugger side visualizer (i.e., DataContract, XML, binary, or SOAP) would need to implement so that the UI can work with them without knowing the exact visualizer instance it is dealing with. - Properties:
IsEditable
: indicates whether the target object is editable in the visualizer UI; based on this the UI will either show or hide an editable interface (a text box in this case).TargetObject
: holds the object received from the debuggee side.FormattedString
: returns a formatted string representing the target object.Serializer
: the serializer the visualizer will use to serialize/deserialize the target object.Name
: name of the visualizer that will appear in the UI title.IsUpdateRequired
: indicates whether the debuggee side object needs to be updated with the debugger side version of the object.- Methods:
SaveToFile
: saves the target object into a fileLoadFromFile
: loads and replaces the debugger side version of the target object.UpdateTargetObject
: updates the debugger side verison of the object.
The code snippet below lists the definition for the Show()
method in the DebuggerSideDataContractVisualizer
class:
/// <summary>
/// Displays the user interface for the visualizer.
/// </summary>
/// <param name="windowService">An object of type
/// Microsoft.VisualStudio.DebuggerVisualizers.IDialogVisualizerService,
/// which provides methods that a visualizer can use
/// to display Windows forms, controls, and dialogs.</param>
/// <param name="objectProvider">An object of type
/// Microsoft.VisualStudio.DebuggerVisualizers.IVisualizerObjectProvider.
/// This object provides communication from the debugger side of the visualizer
/// to the object source (Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource)
/// on the debuggee side.</param>
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
try
{
// Get the object to display a visualizer for.
using(StreamReader streamReader = new StreamReader(objectProvider.GetData()))
{
string targetObjectType = streamReader.ReadLine();
_targetObjectType = Type.GetType(targetObjectType);
TargetObject = Serializer.Deserialize(_targetObjectType, streamReader.ReadToEnd());
}
}
catch (System.Exception exception)
{
MessageBox.Show(string.Format(
Properties.Resources.DeserializationOfXmlFailed, exception.Message));
return;
}
//Display the object in a UI.
using (frmVisualizerDialog displayForm = new frmVisualizerDialog(this))
{
windowService.ShowDialog(displayForm);
if (IsUpdateRequired == true)
{
if (objectProvider.IsObjectReplaceable)
{
//If the debuggee side object is replaceable and it needs to be updated then
//replace it with the target object(debugger side) .
using (MemoryStream outgoingData = new MemoryStream())
{
using (StreamWriter writer = new StreamWriter(outgoingData))
{
writer.WriteLine(TargetObject.GetType().AssemblyQualifiedName);
writer.WriteLine(Serializer.Serialize(TargetObject));
writer.Flush();
objectProvider.ReplaceData(outgoingData);
}
}
}
}
}
}
Debugger Side UI
The debugger side UI is a Windows Form with a textbox and a couple of buttons.
Using the DataContract Visualizer
Add the visualizer attribute based on your scenario:
- Scenario 1: If you can modify the DataContract class file then copy and paste the visualizer attribute (highlighted in bold italics below) to the DataContract class as shown below.
[DebuggerVisualizer(
@"DebuggerUtility.Visualizers.DataContract.DebuggerSideDataContractVisualizer, "
+ @"DebuggerUtility.Visualizers, "
+ @"Version=1.0.0.0, Culture=neutral, "
+ @"PublicKeyToken=e8c91feafdcfb6e2",
@"DebuggerUtility.Visualizers.DataContract.DataContractVisualizerObjectSource, "
+ @"DebuggerUtility.Visualizers, "
+ @"Version=1.0.0.0, Culture=neutral, "
+ @"PublicKeyToken=e8c91feafdcfb6e2",
Description = "DataContractVisualizer ")]
[DataContract]
public class MyDataContract
{
...
...
}
using DebuggerUtility.Visualizers.DataContract;
using System.Diagnostics;
[assembly: DebuggerVisualizer(typeof(DebuggerSideDataContractVisualizer),
typeof(DataContractVisualizerObjectSource),
TargetTypeName = "DebuggerUtility.Visualizers.Tests.MyDataContract,
DebuggerUtility.Visualizers.Tests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
Description = "DataContract Visualizer")]
namespace DebuggerUtility.Visualizers.DataContract
{
…
…
/// <summary>
/// The debugger side class for DataContract visualizer .
/// </summary>
public class DebuggerSideDataContractVisualizer : DebuggerSideVisualizer
{
…
…
}
}
Note:
- The visualizer attribute needs to be added just above the namespace definition as shown above.
- For the
TargetTypeName
you need to fill in the assembly qualified name of your DataContract object. - Compile the code and generate the DLL file DebuggerUtility.Visualizers.dll.
- Once you make sure that the attributes are added correctly depending on your scenario, make sure “DebuggerUtility.Visualizers.dll” is installed as mentioned in the installation section below.
- Start the Visual Studio debugger and bring the control past an instantiated instance of the target class.
- Hover the mouse over the instance and choose the “DataContractVisualizer”.
This brings up the below visualizer interface:
- The visualizer interface contains a text box that displays the serialized string equivalent of the target object.
- The serialized string present in the text box is editable.
- Clicking the "Update" button will update the debugged object with the contents from the text box.
- Copy the DLL “DebuggerUtility.Visualizers.dll” to either of the following locations:
- <VS2010 InstallPath>\Microsoft Visual Studio 10.0\Common7\Packages\Debugger\Visualizers
- My Documents\Visual Studio 2010\Visualizers
- To use the visualizer for remote debugging, copy the DLL to the same path on the remote computer.
- Restart the Visual Studio debugging session.
- The DataContract and XML Visualizers don’t need the objects to be serializable.
- The binary and SOAP visualizers will need the objects to be marked as “
Serialiable
”. - You can use the visualizer on an object of any managed class except
Object
andArray
. - To debug using a visualizer, you must run the code with Full Trust.