65.9K
CodeProject 正在变化。 阅读更多。
Home

CodeDom CodeObject 调试器可视化工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (5投票s)

2009年7月5日

CPOL

7分钟阅读

viewsIcon

28973

downloadIcon

731

使用 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 具体类,指定要使用的 CodeDomProviderDialogDebuggerVisualizer 接收一个简单的可序列化结构体进行显示,其中包含语言名称和输出。
使用几次后,我发现无法从对话框中选择语言、CodeGeneratorOptions 或我当时想要的任何辅助方法,这让我感到恼火。
一次不成功的尝试使用 IVisualizerObjectProvider.TransferObject / VisualizerObjectSource.TransferData 方法,使我设计了一个非常巧妙的方案,滥用了 ReplaceObject/CreateReplacementObject 来达到我的目的。它工作得很完美,但仍然有一个挥之不去的感觉:微软 intended TransferData 的使用是用于调试目标和调试器之间的双向通信。

Microsoft 使得为 Visual Studio 实现自定义调试器可视化工具相对容易,网上也有大量示例。据我所知(AFAIG - As Far As I Can Google...)... - 没有如何正确使用 TransferData (MSDN) 方法的示例。
这里有一个,你会发现它很容易为调试体验带来一点便利。

参考文献

背景

我选择在 VisualizerObjectSource.TransferDataDebuggerVisualizer.Show 方法中处理调试目标和调试器之间的所有通信。而不是使用不可序列化的目标(CodeObject),以下类型被流式传输:

流式传输的可序列化类型

Serializable types

TargetInfo

  • 关于被调试目标的不可变补充信息。
  • DebuggerVisualizer 使用 typeof(TargetInfo) 请求,由 VisualizerObjectSource 创建并返回。
  • 通过类型指定允许扩展 VisualizerObjectSource 以创建和返回类似的类型,这些类型不依赖于设置。

设置

  • 设置由 DebuggerVisualizerEnvDTE.Globals 对象中检索和持久化,作为 VS 范围的设置。
  • DebuggerVisualizer 传输到 VisualizerObjectSource
  • CodeDom.CodeGeneratorOptions 类是不可序列化的,请参见下一节。

表示 

  • DebuggerVisualizer 显示的被调试目标的可视表示。
  • DebuggerVisualizer 发送初始/更新的设置或命令时,由 VisualizerObjectSource 返回。
  • 使用一个 struct 而不是直接序列化 CompilerOutput 字段,为将来的扩展性和重用性提供了一点抽象。

ExtraCommands 枚举

  • 指定额外的命令,以不同于 CodeDomProvider.GenerateCodeXXX 方法返回的方式来评估目标。
  • DebuggerVisualizer 传输到 VisualizerObjectSource
  • 我实现了 CodeTypeDeclaration 中包含成员的两种不同概览,以及 CodeObject.UserData 内容的列表。

可序列化的 CodeGeneratorOptions

SerializableGeneratorOptions class diagramm

CodeDom.CodeGeneratorOptions 是一个简单的类,只有三个布尔属性和两个 string 属性。但是它默认是不可序列化的,因为它导出了一个索引器。CodeGeneratorOptions 不是密封的,所以我的 SerializableGeneratorOptions 愉快地打破了单一职责原则,以满足多个内部需求。

  • null 和默认值的相等性

    使用具有默认值(缩进=4个空格等)或 nullCodeGeneratorOptions 对象在生成的代码中具有相等的效应。SerializableGeneratorOptions 有一个 IsDefault 属性,(反)序列化过程使用一个特殊常量来表示 null 值。

    private const string Format_Null = "null";
  • 序列化为格式化的 string,适用于 System.Configuration.CommaDelimitedStringCollection

    我通常使用 CommaDelimitedStringCollectionCommaDelimitedStringCollectionConverter 在单个字符串键/值对中存储多个设置到 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]);

调试器端

Debugger side

演出开始

点击小放大镜会促使 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

窗体仅将其控件与包装器的内容同步,并在用户选择时触发单独的 SettingsChangedCommandInvoked 事件。
可以选择从编译器输出中删除行号指示符和空行。由于这独立于编译器设置,窗体会使用 EnvDTE.Globals 中的一个单独的键来持久化用户选择。窗体的桌面边界未持久化(我的标准便利功能),因为它会随着编译器输出自动调整大小。虽然这是一个只读可视化工具,但我让文本框可编辑,这有助于生成截图。

CodeObjectVisualizerObjectSource

Debuggee side

上面所有对 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 个继承自 CodeObjectCodeDom 类型中,只有 CodeNamespaceImportCodeDirective 不受此可视化工具支持。CodeDom 集合类型仍然需要一个合适的可视化工具。你的工作?

历史

  • 2009 年 7 月 5 日:发布文章
© . All rights reserved.