Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CodeDom CodeObject Debugger Visualizer

0.00/5 (No votes)
5 Jul 2009 2  
Using VisualizerObjectSource.TransferData for private communication with the DialogDebuggerVisualizer

Introduction

To diagnose CodeDom versus Reflection issues in the WinForms design environment, I had been littering code for some time with Print messages like:

Debug.Print("{0}", OC.VisualStudio.CodeDomHelper.GenerateCodeFromType
						(codeTypeDeclaration));

Only after days I realized, that most of the information I needed at the breakpoint, could better be provided by a debugger visualizer, which I expected to find in quantity on the web. To my surprise, 'Don't be evil' only returned Omer van Kloeten's work, (download unavailable and Omer is on the road) plus Greg Beech's blog entry, which is elegant nonsense but may work for simple (emitted?) CodeObject's handled by default serialization.

The objects I am dealing with originate from internal WinForms serialization infrastructure, with attached eventhandlers and populated CodeObject.UserData dictionaries, thus they need a serializable wrapper or a custom serialization scheme to get across the process boundaries inherent to the debugger visualizer architecture.

My initial solution consisted of an abstract VisualizerObjectSource class, that generated the compiler output for the CodeObject target on the debuggee side and two C# and VB concrete classes specified the CodeDomProvider to use. The DialogDebuggerVisualizer received for display a simple serializable struct, containing language name and output.
After using this a few times, it annoyed me that I could not choose from dialog the language, CodeGeneratorOptions nor any helper methods I wanted on the spot.
An unsuccessful attempt of using the IVisualizerObjectProvider.TransferObject / VisualizerObjectSource.TransferData methods, led me to devise a terrible clever scheme of misusing ReplaceObject/CreateReplacementObject for my means. It worked like a charm, but there remained this nagging feeling: Microsoft intended the usage of TransferData for two-way communication between debuggee and debugger.

Microsoft made it fairly easy to implement a custom debugger visualizer for Visual Studio and there are numerous samples on the web. AFAIG - as far as I can Google ... - there exists no example on how to properly use the TransferData (MSDN) method.
Here is one, you will see it's easy to bring a little convenience to the debugging experience.

References

Background

I choose to handle all communication between debuggee and debugger, in VisualizerObjectSource.TransferData and DebuggerVisualizer.Show methods respectively. Instead of the non-serializable target (CodeObject), the following types are streamed:

Streamed Serializable Types

Serializable types

TargetInfo

  • Invariant supplemental information on the debugged target.
  • Requested with typeof(TargetInfo) from DebuggerVisualizer once, created and returned by VisualizerObjectSource.
  • Specifying by type allows extending VisualizerObjectSource to create and return similar types, that don't depend on settings.

Settings

  • Settings are retrieved and persisted by DebuggerVisualizer in the EnvDTE.Globals object as a VS wide setting.
  • Transferred from DebuggerVisualizer to VisualizerObjectSource.
  • The CodeDom.CodeGeneratorOptions class is not serializable, see following section.

Representation 

  • The visual representation of the debugged target, as displayed by DebuggerVisualizer.
  • Returned by VisualizerObjectSource, when DebuggerVisualizer sends initial/ updated settings or a command.
  • Using a struct, instead of serializing the CompilerOutput field directly, provides a bit of abstraction for future extensibility and re-usage.

ExtraCommands enum

  • Specifies additional commands to evaluate the target differently to what CodeDomProvider.GenerateCodeXXX method returns.
  • Transferred from DebuggerVisualizer to VisualizerObjectSource.
  • I implemented 2 different overviews of the members contained in a CodeTypeDeclaration and a listing of the CodeObject.UserData contents.

Serializable CodeGeneratorOptions

SerializableGeneratorOptions class diagramm

CodeDom.CodeGeneratorOptions is a simple class, having only three boolean and two string properties. However it is not serializable by default, as it exports an indexer. CodeGeneratorOptions is not sealed, so my SerializableGeneratorOptions happily breaks Single Responsibility principle to serve multiple internal requirements.

  • Equality of null and default values

    Using a CodeGeneratorOptions object having the default values (indent= 4 spaces, etc.) or null has equal effect in generated code. SerializableGeneratorOptions has a IsDefault property, and (de-)serialization process uses a special constant to denote a null value.

    private const string Format_Null = "null";
  • Serialize to formatted string, suitable for System.Configuration.CommaDelimitedStringCollection

    I generally use CommaDelimitedStringCollection and CommaDelimitedStringCollectionConverter to store multiple settings in the EnvDTE.Globals cache inside a single string key/value pair. A single setting should ideally be a string, or easily convert to a string, that does not contain commas. The scheme requires strings with non-zero length.

    /// <returns>Returned string is never null, empty or contains commas.</returns>
    public override string ToString()
    {
        if (IsDefault)
            return Format_Null;
    
        return string.Format(CultureInfo.InvariantCulture, "{0};{1};{2};{3};{4}",
            Convert.ToInt32(BlankLinesBetweenMembers),
            BracingStyle,
            Convert.ToInt32(ElseOnClosing),
            IndentString.Length,
            Convert.ToInt32(VerbatimOrder));
    }
  • Construct from formatted string:
    internal SerializableGeneratorOptions(string format)
    {
        if (format == Format_Null)
            // OK: base initialized with default values
            return;
    
        string[] properties = format.Split(';');
    
        BlankLinesBetweenMembers = Convert.ToBoolean(Convert.ToInt32(properties[0]));
        BracingStyle = properties[1];
        ElseOnClosing = Convert.ToBoolean(Convert.ToInt32(properties[2]));
        IndentString = new string(' ', Convert.ToInt32(properties[3]));
        VerbatimOrder = Convert.ToBoolean(Convert.ToInt32(properties[4]));
    }
  • (De-)Serialization with BinaryFormatter:

    The formatted string could be streamed between debuggee and debugger. In this case, I choose to stream the class itself, as it's now easy to use default (de-)serialization by implementing ISerializable and adding the SerializableAttribute.

    [Serializable]
    internal class SerializableGeneratorOptions : CodeGeneratorOptions, ISerializable
    {
        // used by BinaryFormatter.Deserialize()
        protected SerializableGeneratorOptions
    		(SerializationInfo info, StreamingContext context)
            : this(info.GetString("myFORMAT"))
        {}
    
        // used by BinaryFormatter.Serialize()
        void ISerializable.GetObjectData
    	(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("myFORMAT", this.ToString());
        }
    }
  • Convenience static methods:
    internal static SerializableGeneratorOptions FromFormatString(string format)
    {
        if (format == Format_Null)
            return null;
        return new SerializableGeneratorOptions(format);
    }
    
    internal static string ToFormatString
    	(SerializableGeneratorOptions serializableGeneratorOptions)
    {
        if (serializableGeneratorOptions == null || 
    		serializableGeneratorOptions.IsDefault)
            return Format_Null;
        return serializableGeneratorOptions.ToString();
    }

    This permits the external (re-)store logic a direct processing, without worrying about null or default values.

    CommaDelimitedStringCollection col = new CommaDelimitedStringCollection();
    // writeGlobals
    col.Add(SerializableGeneratorOptions.ToFormatString(settings.CodeGeneratorOptions));
    // readGlobals
    settings.CodeGeneratorOptions = 
    	SerializableGeneratorOptions.FromFormatString(col[1]);

Debugger Side

Debugger side

Let the Show Begin

A click on the tiny magnifying glass causes Visual Studio to invoke our CodeObjectVisualizer.Show method, passing in an IVisualizerObjectProvider implementation. CodeObjectVisualizer never gets access to the real debugged target, it only works on the streamable wrappers, returned by IVisualizerObjectProvider.TransferObject.

Sanity omitted plus pseudocode:

// Debugger Side: runs within the VS debugger process
internal class CodeObjectVisualizer : DialogDebuggerVisualizer
{
    /// <summary>Shows the user interface for this visualizer.</summary>
    /// <param name="windowService">
    /// An object of type <see cref="IDialogVisualizerService"/>, which provides methods
    /// this visualizer can use to display Windows forms, controls, and dialogs.
    /// </param>
    /// <param name="objectProvider">
    /// An object of type <see cref="IVisualizerObjectProvider"/>.
    /// This object provides communication from the debugger sideof the visualizer
    /// to the object source (<see cref="VisualizerObjectSource"/>) on the debuggee side.
    /// </param>
    protected override void Show(
        IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
    {
        Settings settings = (read persisted Settings from Globals) ?? 
						(use default Settings);

        using (VisualizerForm form = new VisualizerForm())
        {
            // initialize form controls
            form.TargetInfo = (TargetInfo)objectProvider.TransferObject
						(typeof(TargetInfo));
            form.Settings = settings;

            // transfer persisted settings, VisualizerObjectSource returns generated code
            form.Representation = (Representation)objectProvider.TransferObject(settings);

            form.SettingsChanged += delegate //(object sender, EventArgs e)
            {
                // settings changed by user
                // update VisualizerObjectSource with current settings 
	       // and show new generated code
                form.Representation = 
		(Representation)objectProvider.TransferObject(settings);
            };

            form.CommandInvoked += delegate //(object sender, EventArgs e)
            {
                // transfer command to VisualizerObjectSource and show generated result
                form.Representation = 
			(Representation)objectProvider.TransferObject(form.Command);
            };

            windowService.ShowDialog(form);
        }

        (persist settings)
    }
}

Note that the streamed types are passed on to the form, CodeObjectVisualizer is practically boilerplate code.

VisualizerForm

The form just synchronizes its controls with wrapper's contents, and fires separate SettingsChanged, CommandInvoked events on user choice.
Optionally line pragmas and blank lines are removed from the compiler output. As this is independent of compiler settings, the form persists user choice with a separate key in EnvDTE.Globals. The form's desktop bounds are not persisted (my standard convenience feature), since it automatically resizes with the compiler output. Although this is a read-only visualizer, I let the textbox be editable, which helped to produce the screenshot.

CodeObjectVisualizerObjectSource

Debuggee side

All calls above of IVisualizerObjectProvider.TransferObject ultimately invoke the VisualizerObjectSource.TransferData on the debuggee side, with incoming and empty outcoming MemoryStreams. The base implementation just throws an exception (my initial confusion), yet VisualizerObjectSource provides the static Serialize/Deserialize helper methods to keep it easy.

Sanity omitted plus pseudocode:

// Debuggee Side: runs within debugged program's process
internal class CodeObjectVisualizerObjectSource : VisualizerObjectSource
{
    readonly Settings curSettings = new Settings();
    private CodeDomProvider compiler;
    private CodeGeneratorOptions codeGeneratorOptions;
    private CodeObject debuggedCodeObject;

    /// <summary>
    /// Transfers data simultaneously in both directions 
    /// between the debuggee and debugger sides.
    /// </summary>
    /// <remarks>
    /// The data may be any sort of request for the visualizer, 
    /// whether to fetch data incrementally,
    /// or to update the state of the object being visualized.
    /// The transfer is always initiated by the debugger side.
    /// </remarks>
    /// <param name="target">Object being visualized.</param>
    /// <param name="incomingData">Incoming data stream from the debugger side.</param>
    /// <param name="outgoingData">Outgoing data stream going to the debugger side.
    /// </param>
    public override void TransferData(object target, Stream incomingData, 
						Stream outgoingData)
    {
        // target is always a CodeObject: store target once
        this.debuggedCodeObject = (CodeObject)target;

        object incoming = Deserialize(incomingData);

        if (incoming is typeof(TargetInfo))
        {
            // create TargetInfo
            TargetInfo targetInfo = new TargetInfo{(invariant target data)};
            Serialize(outgoingData, targetInfo);
            return;
        }

        Representation representation = new Representation();

        Settings settings = incoming as Settings;
        if (settings != null)
        {
            // initialize or update settings, stored in curSettings field
            createCompiler(settings.LanguageName);
            createCodeGeneratorOptions(settings.CodeGeneratorOptions);

            // generate code based on current settings
            representation.CompilerOutput = generateCode(debuggedCodeObject);
        }

        ExtraCommands? command = incoming as ExtraCommands?;
        if (command.HasValue)
        {
            // use custom method specified by command
            representation.CompilerOutput = commandMethod();
        }

        Serialize(outgoingData, representation);
    }
}

Note that I did not use the common IVisualizerObjectProvider.GetObject / VisualizerObjectSource.GetData at all. Still free to use, but VisualizerObjectSource.TransferData provides all opportunities.

Finally the DebuggerVisualizerAttribute specifies our types at the assembly level.

[assembly: DebuggerVisualizer(typeof(CodeObjectVisualizer), 
	typeof(CodeObjectVisualizerObjectSource),
    	Description = "CodeDom CodeObject Visualizer", Target = typeof(CodeObject))]

Accessing Globals Object

Microsoft provides a VisualizerDevelopmentHost to simplify debugging of the Visualizer in the IDE. A difference to real live debugging -- insert System.Diagnostics.Debugger.Break() -- is that CodeObjectVisualizer runs out-of-proc, instead of within the VS debugger process.
Accessing the EnvDTE.Globals object is now susceptible to thrown ComExceptions, with errorcode RPC_E_SERVERCALL_RETRYLATER [MTAThread] or RPC_E_CALL_REJECTED [STAThread]. As others before, I noticed that this susceptibility seems to have increased with VS2008. I took the opportunity to rewrite my GlobalsHelper class for safe out-of-proc operation, all methods now retry 3 times (once seems enough), before rethrowing the specific ComException.
This is irrelevant for the released DLL, just remember to access any VS automation objects only from the debugger side.

Using the Code

Drop the compiled DLL in ..\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers or the user-specific MyDocuments\Visual Studio 2008\Visualizers folder. When debugging and at a breakpoint, the little magnifying glass will appear in the datatip for all objects, deriving from CodeObject.

The VS 2005 version should work, but is untested. A confirmation would be fine.

Points of Interest

4 years after Microsoft introduced debugger visualizers, it should be considered common knowledge, that the target must be serializable. It has to be serializable by default, if you want to Create a Debugger Visualizer in 10 Lines of Code. The simple fact of a non-serializable target is no excuse for the nonexistence of a suitable visualizer. There are many ways to solve this minor problem and some examples are available.

This article promotes enhanced usability features like persisting settings, switching views, autosizing. For a serializable target, these can be implemented solely on the debugger side, otherwise adapt the scheme above. List Visualizer xy, which forces me to manually resize its datagrid, in order to see contents properly, is an insult.

From the 62 CodeDom types that inherit from CodeObject, only CodeNamespaceImport and CodeDirective are not supported by this visualizer. The CodeDom collection types are still in need of a suitable visualizer. Your work?

History

  • 5th July, 2009: Article posted

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here