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 the DebuggerSideVisualizer
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 the VisualizerObjectSource
(see class diagram above) to provide custom implementations of the GetData()
and CreateReplacementObject()
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
.
public class DataContractVisualizerObjectSource : VisualizerObjectSource
{
public DataContractVisualizerObjectSource()
{
serializer = new DataContractSerialization();
}
private SerializationBase serializer;
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();
}
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 from DialogDebuggerVisualizer
. - 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 file LoadFromFile
: 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:
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
try
{
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;
}
using (frmVisualizerDialog displayForm = new frmVisualizerDialog(this))
{
windowService.ShowDialog(displayForm);
if (IsUpdateRequired == true)
{
if (objectProvider.IsObjectReplaceable)
{
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
{
...
...
}
Scenario 2: If you cannot modify the DataContract class then you will need to take the source code attached with this article for visualizers and add the visualizer attribute (highlighted in bold italics below) on the namespace "DebuggerUtility.Visualizers.DataContract" as shown in the snippet below:
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
{
…
…
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.
Installing the Visualizers
- 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.
Points of Interest
- 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
and Array
. - To debug using a visualizer, you must run the code with Full Trust.