Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Capture object state with visualizers

5.00/5 (8 votes)
1 May 2013CPOL6 min read 23.4K   201  
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.

Image 1

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:

  1. Debuggee side
  2. 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:

Image 2

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.

C#
/// <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 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.

Image 3

The code snippet below lists the definition for the Show() method in the DebuggerSideDataContractVisualizer class:

C#
/// <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.

Image 4

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.
  • C#
    [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: 
  • C#
    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”.

Image 5

This brings up the below visualizer interface:

VisualizerUI

  • 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)