CodeDom CodeObject 调试器可视化工具






4.73/5 (5投票s)
使用 VisualizerObjectSource.TransferData 与 DialogDebuggerVisualizer 进行私有通信

引言
为了诊断 WinForms 设计环境中的 CodeDom
与反射问题,我花了不少时间在代码中添加类似这样的 Print 消息:
Debug.Print("{0}", OC.VisualStudio.CodeDomHelper.GenerateCodeFromType
(codeTypeDeclaration));
几天后我才意识到,在断点处我需要的大部分信息,通过调试器可视化工具提供会更好,我本以为网上会有很多这样的工具。令我惊讶的是,‘Don't be evil’(谷歌)只返回了 Omer van Kloeten 的 工作(无法下载,Omer 正在旅行中),以及 Greg Beech 的 博客文章,它优雅得像胡说八道,但对于默认序列化处理的简单(已发出?)CodeObject
可能有效。
我处理的对象源自内部 WinForms 序列化基础结构,带有附加的事件处理程序和填充的 CodeObject.UserData
字典,因此它们需要一个可序列化的包装器或自定义序列化方案,才能跨越调试器可视化工具架构固有的进程边界。
我的初始解决方案包含一个抽象的 VisualizerObjectSource
类,该类在调试目标端生成 CodeObject
的编译器输出,并包含两个 C# 和 VB 具体类,指定要使用的 CodeDomProvider
。DialogDebuggerVisualizer
接收一个简单的可序列化结构体进行显示,其中包含语言名称和输出。
使用几次后,我发现无法从对话框中选择语言、CodeGeneratorOptions
或我当时想要的任何辅助方法,这让我感到恼火。
一次不成功的尝试使用 IVisualizerObjectProvider.TransferObject
/ VisualizerObjectSource.TransferData
方法,使我设计了一个非常巧妙的方案,滥用了 ReplaceObject
/CreateReplacementObject
来达到我的目的。它工作得很完美,但仍然有一个挥之不去的感觉:微软 intended TransferData
的使用是用于调试目标和调试器之间的双向通信。
Microsoft 使得为 Visual Studio 实现自定义调试器可视化工具相对容易,网上也有大量示例。据我所知(AFAIG - As Far As I Can Google...)...
- 没有如何正确使用 TransferData
(MSDN) 方法的示例。
这里有一个,你会发现它很容易为调试体验带来一点便利。
参考文献
背景
我选择在 VisualizerObjectSource.TransferData
和 DebuggerVisualizer.Show
方法中处理调试目标和调试器之间的所有通信。而不是使用不可序列化的目标(CodeObject
),以下类型被流式传输:
流式传输的可序列化类型

TargetInfo
- 关于被调试目标的不可变补充信息。
- 从
DebuggerVisualizer
使用typeof
(TargetInfo
) 请求,由VisualizerObjectSource
创建并返回。 - 通过类型指定允许扩展
VisualizerObjectSource
以创建和返回类似的类型,这些类型不依赖于设置。
设置
- 设置由
DebuggerVisualizer
在EnvDTE.Globals
对象中检索和持久化,作为 VS 范围的设置。 - 从
DebuggerVisualizer
传输到VisualizerObjectSource
。 CodeDom.CodeGeneratorOptions
类是不可序列化的,请参见下一节。
表示
- 由
DebuggerVisualizer
显示的被调试目标的可视表示。 - 当
DebuggerVisualizer
发送初始/更新的设置或命令时,由VisualizerObjectSource
返回。 - 使用一个
struct
而不是直接序列化CompilerOutput
字段,为将来的扩展性和重用性提供了一点抽象。
ExtraCommands 枚举
- 指定额外的命令,以不同于
CodeDomProvider.GenerateCodeXXX
方法返回的方式来评估目标。 - 从
DebuggerVisualizer
传输到VisualizerObjectSource
。 - 我实现了
CodeTypeDeclaration
中包含成员的两种不同概览,以及CodeObject.UserData
内容的列表。
可序列化的 CodeGeneratorOptions

CodeDom.CodeGeneratorOptions
是一个简单的类,只有三个布尔属性和两个 string
属性。但是它默认是不可序列化的,因为它导出了一个索引器。CodeGeneratorOptions
不是密封的,所以我的 SerializableGeneratorOptions
愉快地打破了单一职责原则,以满足多个内部需求。
null
和默认值的相等性使用具有默认值(缩进=4个空格等)或
null
的CodeGeneratorOptions
对象在生成的代码中具有相等的效应。SerializableGeneratorOptions
有一个IsDefault
属性,(反)序列化过程使用一个特殊常量来表示null
值。private const string Format_Null = "null";
- 序列化为格式化的
string
,适用于System.Configuration.CommaDelimitedStringCollection
我通常使用
CommaDelimitedStringCollection
和CommaDelimitedStringCollectionConverter
在单个字符串键/值对中存储多个设置到EnvDTE.Globals
缓存中。单个设置理想情况下应该是一个string
,或者易于转换为string
,并且不包含逗号。该方案要求string
的长度非零。/// <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)); }
- 从格式化的
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])); }
- (反)序列化与
BinaryFormatter
格式化的
string
可以在调试目标和调试器之间进行流式传输。在这种情况下,我选择流式传输类本身,因为现在通过实现ISerializable
并添加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()); } }
- 便捷的
static
方法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(); }
这允许外部(重新)存储逻辑进行直接处理,而无需担心
null
或默认值。CommaDelimitedStringCollection col = new CommaDelimitedStringCollection(); // writeGlobals col.Add(SerializableGeneratorOptions.ToFormatString(settings.CodeGeneratorOptions)); // readGlobals settings.CodeGeneratorOptions = SerializableGeneratorOptions.FromFormatString(col[1]);
调试器端

演出开始
点击小放大镜会促使 Visual Studio 调用我们的 CodeObjectVisualizer.Show
方法,并传递一个 IVisualizerObjectProvider
实现。CodeObjectVisualizer
永远无法访问真实的被调试目标,它只能处理 IVisualizerObjectProvider.TransferObject
返回的可流式传输的包装器。
省略了健全性检查,仅包含伪代码
// 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)
}
}
请注意,流式传输的类型被传递给窗体,CodeObjectVisualizer
实际上是样板代码。
VisualizerForm
窗体仅将其控件与包装器的内容同步,并在用户选择时触发单独的 SettingsChanged
、CommandInvoked
事件。
可以选择从编译器输出中删除行号指示符和空行。由于这独立于编译器设置,窗体会使用 EnvDTE.Globals
中的一个单独的键来持久化用户选择。窗体的桌面边界未持久化(我的标准便利功能),因为它会随着编译器输出自动调整大小。虽然这是一个只读可视化工具,但我让文本框可编辑,这有助于生成截图。
CodeObjectVisualizerObjectSource

上面所有对 IVisualizerObjectProvider.TransferObject
的调用最终都会在调试目标端调用 VisualizerObjectSource.TransferData
,并传入传入和传出的空 MemoryStream
。基类实现只是抛出异常(我最初的困惑),但 VisualizerObjectSource
提供了静态的 Serialize
/Deserialize
辅助方法来简化操作。
省略了健全性检查,仅包含伪代码
// 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);
}
}
请注意,我根本没有使用常见的 IVisualizerObjectProvider.GetObject
/ VisualizerObjectSource.GetData
。仍然可以自由使用,但 VisualizerObjectSource.TransferData
提供了所有机会。
最后,DebuggerVisualizerAttribute
在程序集级别指定了我们的类型。
[assembly: DebuggerVisualizer(typeof(CodeObjectVisualizer),
typeof(CodeObjectVisualizerObjectSource),
Description = "CodeDom CodeObject Visualizer", Target = typeof(CodeObject))]
访问 Globals 对象
Microsoft 提供了一个 VisualizerDevelopmentHost
来简化 IDE 中可视化工具的调试。与真实实时调试的区别——插入 System.Diagnostics.Debugger.Break()
——是 CodeObjectVisualizer
在进程外运行,而不是在 VS 调试器进程内运行。
访问 EnvDTE.Globals
对象现在容易因 ComException
而引发异常,错误代码为 RPC_E_SERVERCALL_RETRYLATER [MTAThread]
或 RPC_E_CALL_REJECTED [STAThread]
。正如其他人之前所说,我注意到这种易受攻击性似乎随着 VS2008 而增加。我抓住机会重写了我的 GlobalsHelper
类以实现安全的进程外操作,所有方法现在都重试 3 次(一次似乎就足够了),然后再重新抛出特定的 ComException
。
这对于发布的 DLL 是不相关的,只需记住只从调试器端访问任何 VS 自动化对象。
Using the Code
将编译好的 DLL 放入 ..\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers 文件夹或用户特定的 MyDocuments\Visual Studio 2008\Visualizers 文件夹。在调试和断点处,数据提示中会出现一个小放大镜,用于所有派生自 CodeObject
的对象。
VS 2005 版本应该可以工作,但未经测试。得到确认将是好的。
关注点
在 Microsoft 引入调试器可视化工具 4 年后,应该认为目标必须是可序列化的是常识。如果你想 用 10 行代码创建调试器可视化工具,它必须是默认可序列化的。一个不可序列化目标这个简单的事实,并不能成为缺乏合适可视化工具的借口。有很多方法可以解决这个小问题,并且有一些例子可用。
本文推广了增强的可用性功能,如持久化设置、切换视图、自动调整大小。对于可序列化的目标,这些功能可以完全在调试器端实现,否则请调整上述方案。List Visualizer xy,它迫使我手动调整其数据网格大小才能正确查看内容,这是一种侮辱。
在 62 个继承自 CodeObject
的 CodeDom
类型中,只有 CodeNamespaceImport
和 CodeDirective
不受此可视化工具支持。CodeDom
集合类型仍然需要一个合适的可视化工具。你的工作?
历史
- 2009 年 7 月 5 日:发布文章