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
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
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 string
s with non-zero length.
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)
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
{
protected SerializableGeneratorOptions
(SerializationInfo info, StreamingContext context)
: this(info.GetString("myFORMAT"))
{}
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();
col.Add(SerializableGeneratorOptions.ToFormatString(settings.CodeGeneratorOptions));
settings.CodeGeneratorOptions =
SerializableGeneratorOptions.FromFormatString(col[1]);
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:
internal class CodeObjectVisualizer : DialogDebuggerVisualizer
{
protected override void Show(
IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
{
Settings settings = (read persisted Settings from Globals) ??
(use default Settings);
using (VisualizerForm form = new VisualizerForm())
{
form.TargetInfo = (TargetInfo)objectProvider.TransferObject
(typeof(TargetInfo));
form.Settings = settings;
form.Representation = (Representation)objectProvider.TransferObject(settings);
form.SettingsChanged += delegate
{
form.Representation =
(Representation)objectProvider.TransferObject(settings);
};
form.CommandInvoked += delegate
{
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
All calls above of IVisualizerObjectProvider.TransferObject
ultimately invoke the VisualizerObjectSource.TransferData
on the debuggee side, with incoming and empty outcoming MemoryStream
s. 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:
internal class CodeObjectVisualizerObjectSource : VisualizerObjectSource
{
readonly Settings curSettings = new Settings();
private CodeDomProvider compiler;
private CodeGeneratorOptions codeGeneratorOptions;
private CodeObject debuggedCodeObject;
public override void TransferData(object target, Stream incomingData,
Stream outgoingData)
{
this.debuggedCodeObject = (CodeObject)target;
object incoming = Deserialize(incomingData);
if (incoming is typeof(TargetInfo))
{
TargetInfo targetInfo = new TargetInfo{(invariant target data)};
Serialize(outgoingData, targetInfo);
return;
}
Representation representation = new Representation();
Settings settings = incoming as Settings;
if (settings != null)
{
createCompiler(settings.LanguageName);
createCodeGeneratorOptions(settings.CodeGeneratorOptions);
representation.CompilerOutput = generateCode(debuggedCodeObject);
}
ExtraCommands? command = incoming as ExtraCommands?;
if (command.HasValue)
{
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 ComException
s, 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