创建 Visual Studio 调试器可视化工具

调试器可视化工具是一项 Visual Studio 功能,在调试会话期间为特定 .NET 类型的变量或对象提供自定义可视化效果。

调试器可视化工具可以通过将鼠标悬停在变量上时出现的DataTip,或从自动局部变量监视窗口访问。

监视窗口中调试器可视化工具的屏幕截图。

开始

请按照“入门”部分中的创建扩展项目小节进行操作。

然后,添加一个扩展 DebuggerVisualizerProvider 的类,并将 VisualStudioContribution 属性应用于该类。

/// <summary>
/// Debugger visualizer provider class for <see cref="System.String"/>.
/// </summary>
[VisualStudioContribution]
internal class StringDebuggerVisualizerProvider : DebuggerVisualizerProvider
{
    /// <summary>
    /// Initializes a new instance of the <see cref="StringDebuggerVisualizerProvider"/> class.
    /// </summary>
    /// <param name="extension">Extension instance.</param>
    /// <param name="extensibility">Extensibility object.</param>
    public StringDebuggerVisualizerProvider(StringDebuggerVisualizerExtension extension, VisualStudioExtensibility extensibility)
        : base(extension, extensibility)
    {
    }

    /// <inheritdoc/>
    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("My string visualizer", typeof(string));

    /// <inheritdoc/>
    public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
    {
        string targetObjectValue = await visualizerTarget.ObjectSource.RequestDataAsync<string>(jsonSerializer: null, cancellationToken);

        return new MyStringVisualizerControl(targetObjectValue);
    }
}

前面的代码定义了一个新的调试器可视化工具,该可视化工具适用于类型如下 string的对象:

  • DebuggerVisualizerProviderConfiguration 属性定义可视化工具显示名称和支持的 .NET 类型。
  • CreateVisualizerAsync当用户请求显示特定值的调试器可视化工具时,Visual Studio 将调用该方法。 CreateVisualizerAsync VisualizerTarget使用对象检索要可视化的值,并将其传递给自定义远程用户控件(引用远程 UI 文档)。 然后返回远程用户控件,并将显示在 Visual Studio 的弹出窗口中。

面向多个类型

配置属性允许可视化工具在方便时面向多个类型。 一个完美的示例是支持可视化DataSetDataTableDataView对象的DataViewManager。 此功能简化了扩展开发,因为类似的类型可以共享相同的 UI、视图模型和 可视化工具对象源

    /// <inheritdoc/>
    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new DebuggerVisualizerProviderConfiguration(
        new VisualizerTargetType("DataSet Visualizer", typeof(System.Data.DataSet)),
        new VisualizerTargetType("DataTable Visualizer", typeof(System.Data.DataTable)),
        new VisualizerTargetType("DataView Visualizer", typeof(System.Data.DataView)),
        new VisualizerTargetType("DataViewManager Visualizer", typeof(System.Data.DataViewManager)));

    /// <inheritdoc/>
    public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
    {
        ...
    }

可视化工具对象源

可视化工具对象源是调试过程中调试器加载的 .NET 类。 调试器可视化器可以使用VisualizerTarget.ObjectSource公开的方法从可视化器对象源中检索数据。

默认可视化工具对象源允许调试器可视化工具通过调用 RequestDataAsync<T>(JsonSerializer?, CancellationToken) 该方法检索要可视化的对象的值。 默认可视化工具对象源使用 Newtonsoft.Json 序列化值,VisualStudio.Extensibility 库也使用 Newtonsoft.Json 进行反序列化。 或者,可以使用RequestDataAsync(CancellationToken)将序列化值作为JToken进行检索。

如果要可视化 Newtonsoft.Json 本机支持的 .NET 类型,或者想要可视化自己的类型,并且可以使其可序列化,前面的说明足以创建简单的调试器可视化工具。 如果想要支持更复杂的类型或使用更高级的功能,请继续阅读。

使用自定义可视化工具对象源

如果要可视化的类型不能由 Newtonsoft.Json 自动序列化,则可以创建自定义可视化工具对象源来处理序列化。

  • 创建一个面向netstandard2.0的新的 .NET 类库项目。 如果需要序列化要可视化的对象,可以将更具体的 .NET Framework 或 .NET 版本(例如 net472 ,或 net6.0)作为目标。
  • 添加对 DebuggerVisualizers 版本 17.6 或更高版本的包引用。
  • 创建一个继承自VisualizerObjectSource的类,并重写GetData,将target的序列化值写入outgoingData流中。
public class MyObjectSource : VisualizerObjectSource
{
    /// <inheritdoc/>
    public override void GetData(object target, Stream outgoingData)
    {
        MySerializableType result = Convert(match);
        SerializeAsJson(outgoingData, result);
    }

    private static MySerializableType Convert(object target)
    {
        // Add your code here to convert target into a type serializable by Newtonsoft.Json
        ...
    }
}

使用自定义序列化

可以使用该方法 VisualizerObjectSource.SerializeAsJson 使用 Newtonsoft.Json 将对象序列化为 a Stream ,而无需将对 Newtonsoft.Json 的引用添加到库中。 SerializeAsJson调用将通过反射将 Newtonsoft.Json 程序集的版本加载到正在调试的进程中。

如果需要引用 Newtonsoft.Json,则应使用包引用 Microsoft.VisualStudio.Extensibility.Sdk 的相同版本,但最好使用 DataContract 属性 DataMember 来支持对象序列化,而不是依赖 Newtonsoft.Json 类型。

或者,也可以实现自己的自定义序列化(如二进制序列化)直接写入 outgoingData

将可视化工具对象源 DLL 添加到扩展

ProjectReference 添加到扩展 .csproj 文件中,并将其添加到可视化对象源库项目,这可确保在打包扩展之前先生成可视化对象源库。

此外,将包含 Content 可视化工具对象源库 DLL 的项添加到 netstandard2.0 扩展的子文件夹中。

  <ItemGroup>
    <Content Include="pathToTheObjectSourceDllBinPath\$(Configuration)\netstandard2.0\MyObjectSourceLibrary.dll" Link="netstandard2.0\MyObjectSourceLibrary.dll">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyObjectSourceLibrary\MyObjectSourceLibrary.csproj" />
  </ItemGroup>

或者,如果生成面向 .NET Framework 或 .NET 的可视化工具对象源库,则可以使用 net4.6.2netcoreapp 子文件夹。 你甚至可以包含具有不同版本的可视化对象源库的所有三个子文件夹,但最好只针对 netstandard2.0

应尽量减少可视化工具对象源库 DLL 的依赖项数。 如果可视化工具对象源库具有 除Microsoft.VisualStudio.DebuggerVisualizer 和 已保证在调试过程中加载的库以外的依赖项,请确保还将这些 DLL 文件包含在可视化工具对象源库 DLL 所在的同一子文件夹中。

更新调试器可视化提供程序以使用自定义对象源

然后,可以更新 DebuggerVisualizerProvider 配置以引用自定义可视化工具对象源:

    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("My visualizer", typeof(TypeToVisualize))
    {
        VisualizerObjectSourceType = new(typeof(MyObjectSource)),
    };

    public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
    {
        MySerializableType result = await visualizerTarget.ObjectSource.RequestDataAsync<MySerializableType>(jsonSerializer: null, cancellationToken);
        return new MyVisualizerUserControl(result);
    }

使用大型复杂对象

如果无法通过对 RequestDataAsync 的单次无参数调用来检索可视化工具对象源中的数据,则可以通过多次调用 RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) 并发送不同的 消息 到可视化工具对象源,以执行与可视化工具对象源的更复杂的消息交换。 消息和响应都由使用 Newtonsoft.Json 的 VisualStudio.Extensibility 基础结构序列化。 RequestDataAsync 的其他重写允许您使用 JToken 对象或实现自定义序列化和反序列化。

可以使用不同的消息实现任何自定义协议,以从可视化工具对象源检索信息。 此功能的最常见用例是将潜在大型对象的检索分解为多个调用,以避免 RequestDataAsync 超时。

这是一个示例,演示如何一次检索一个可能较大的集合的内容:

for (int i = 0; ; i++)
{
    MySerializableType? collectionEntry = await visualizerTarget.ObjectSource.RequestDataAsync<int, MySerializableType?>(i, jsonSerializer: null, cancellationToken);
    if (collectionEntry is null)
    {
        break;
    }

    observableCollection.Add(collectionEntry);
}

代码如上,使用简单索引作为 RequestDataAsync 调用的消息。 相应的可视化工具对象源代码将替代 TransferData 方法(而不是 GetData):

public class MyCollectionTypeObjectSource : VisualizerObjectSource
{
    public override void TransferData(object target, Stream incomingData, Stream outgoingData)
    {
        var index = (int)DeserializeFromJson(incomingData, typeof(int))!;

        if (target is MyCollectionType collection && index < collection.Count)
        {
            var result = Convert(collection[index]);
            SerializeAsJson(outgoingData, result);
        }
        else
        {
            SerializeAsJson(outgoingData, null);
        }
    }

    private static MySerializableType Convert(object target)
    {
        // Add your code here to convert target into a type serializable by Newtonsoft.Json
        ...
    }
}

上面的可视化工具对象源利用 VisualizerObjectSource.DeserializeFromJson 方法反序列化来自incomingData的消息,该消息由可视化工具提供程序发送。

实现执行与可视化工具对象源进行复杂消息交互的调试器可视化工具提供程序时,通常最好将 VisualizerTarget 传递给可视化工具的 RemoteUserControl,以便在加载控件时异步进行消息交换。 VisualizerTarget 通过传递,也可以将消息发送到可视化工具对象源,以根据用户与可视化工具 UI 的交互来检索数据。

public override Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
{
    return Task.FromResult<IRemoteUserControl>(new MyVisualizerUserControl(visualizerTarget));
}
internal class MyVisualizerUserControl : RemoteUserControl
{
    private readonly VisualizerTarget visualizerTarget;

    public MyVisualizerUserControl(VisualizerTarget visualizerTarget)
        : base(new MyDataContext())
    {
        this.visualizerTarget = visualizerTarget;
    }

    public override async Task ControlLoadedAsync(CancellationToken cancellationToken)
    {
        // Start querying the VisualizerTarget here
        ...
    }
    ...

以工具窗口形式打开可视化器

默认情况下,所有调试器可视化工具扩展都作为 Visual Studio 前台的模式对话框窗口打开。 因此,如果用户希望继续与 IDE 交互,则需要关闭可视化工具。 但是,如果 Style 属性在 DebuggerVisualizerProviderConfiguration 属性中设置为 ToolWindow,则可视化工具将作为非模式工具窗口打开,可以在调试会话的其余部分保持开启。 如果未声明任何样式,将使用默认值 ModalDialog

    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("My visualizer", typeof(TypeToVisualize))
    {
        Style = VisualizerStyle.ToolWindow
    };

    public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
    {
        // The control will be in charge of calling the RequestDataAsync method from the visualizer object source and disposing of the visualizer target.
        return new MyVisualizerUserControl(visualizerTarget);
    }

每当可视化工具选择作为ToolWindow打开时,它需要订阅VisualizerTargetStateChanged事件。 当可视化工具作为工具窗口打开时,它不会阻止用户取消暂停调试会话。 因此,每当调试目标的状态发生更改时,调试器就会触发上述事件。 可视化工具扩展作者应特别注意这些通知,因为可视化工具目标仅在调试会话处于活动状态且调试目标暂停时可用。 当可视化对象不可用时,对ObjectSource方法的调用将失败,并出现VisualizerTargetUnavailableException错误。

internal class MyVisualizerUserControl : RemoteUserControl
{
    private readonly VisualizerDataContext dataContext;

#pragma warning disable CA2000 // Dispose objects before losing scope
    public MyVisualizerUserControl(VisualizerTarget visualizerTarget)
        : base(dataContext: new VisualizerDataContext(visualizerTarget))
#pragma warning restore CA2000 // Dispose objects before losing scope
    {
        this.dataContext = (VisualizerDataContext)this.DataContext!;
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.dataContext.Dispose();
        }
    }

    [DataContract]
    private class VisualizerDataContext : NotifyPropertyChangedObject, IDisposable
    {
        private readonly VisualizerTarget visualizerTarget;
        private MySerializableType? _value;
        
        public VisualizerDataContext(VisualizerTarget visualizerTarget)
        {
            this.visualizerTarget = visualizerTarget;
            visualizerTarget.StateChanged += this.OnStateChangedAsync;
        }

        [DataMember]
        public MySerializableType? Value
        {
            get => this._value;
            set => this.SetProperty(ref this._value, value);
        }

        public void Dispose()
        {
            this.visualizerTarget.Dispose();
        }

        private async Task OnStateChangedAsync(object? sender, VisualizerTargetStateNotification args)
        {
            switch (args)
            {
                case VisualizerTargetStateNotification.Available:
                case VisualizerTargetStateNotification.ValueUpdated:
                    Value = await visualizerTarget.ObjectSource.RequestDataAsync<MySerializableType>(jsonSerializer: null, CancellationToken.None);
                    break;
                case VisualizerTargetStateNotification.Unavailable:
                    Value = null;
                    break;
                default:
                    throw new NotSupportedException("Unexpected visualizer target state notification");
            }
        }
    }
}

通知 Available 将在 RemoteUserControl 被创建后立即收到,并且正要在新创建的可视化工具窗口中显示时收到。 只要可视化工具保持打开状态,每次调试目标更改其状态时都可以接收其他 VisualizerTargetStateNotification 值。 通知 ValueUpdated 用于指示可视化工具打开的最后一个表达式已成功重新计算调试器停止的位置,并且应由 UI 刷新。 每当调试目标恢复或在停止后无法重新评估表达式时,系统将会收到 Unavailable 通知。

更新可视化对象值

如果 VisualizerTarget.IsTargetReplaceable 为 true,调试器可视化工具可以使用 ReplaceTargetObjectAsync 该方法更新正在调试的进程中可视化对象的值。

可视化工具对象源必须重写 CreateReplacementObject 方法:

public override object CreateReplacementObject(object target, Stream incomingData)
{
    // Use DeserializeFromJson to read from incomingData
    // the new value of the object being visualized
    ...
    return newValue;
}

尝试该 RegexMatchDebugVisualizer 示例,看看这些技术在实际中的应用效果。