创建 Visual Studio 调试程序可视化工具

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

调试器可视化工具可从将鼠标悬停在变量或“自动”、“局部变量”和“监视”窗口中时出现的 DataTip 进行访问:

Screenshot of debugger visualizers in the watch window.

开始使用

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

然后,添加一个扩展 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 将调用该方法。 CreateVisualizerAsyncVisualizerTarget使用对象检索要可视化的值,并将其传递给自定义远程用户控件(引用远程 UI 文档)。 然后返回远程用户控件,并将显示在 Visual Studio 的弹出窗口中。

面向多个类型

配置属性允许可视化工具在方便时面向多个类型。 一个完美的示例是支持可视化DataSetDataTableDataViewDataViewManager对象的数据集可视化工具。 此功能简化了扩展开发,因为类似的类型可以共享相同的 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/&a0> 检索序列化值。

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

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

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

  • 创建新的 .NET 类库项目目标 netstandard2.0。 如果需要序列化要可视化的对象,可以将更具体的 .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 添加到扩展

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

此外,将包含 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 基础结构序列化。 允许使用JToken对象或实现自定义序列化和反序列化的其他替代RequestDataAsync

可以使用不同的消息实现任何自定义协议,以从可视化工具对象源检索信息。 此功能的最常见用例是将潜在大型对象的检索分解为多个调用,以避免 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 属性设置为 ToolWindow 该属性, DebuggerVisualizerProviderConfiguration 则可视化工具将作为非模式工具窗口打开,该窗口可在调试会话的其余部分保持打开状态。 如果未声明任何样式,将使用默认值 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打开时,它都需要订阅 StateChanged 事件 VisualizerTarget。 当可视化工具作为工具窗口打开时,它不会阻止用户取消暂停调试会话。 因此,每当调试目标的状态发生更改时,调试器就会触发上述提及事件。 可视化工具扩展作者应特别注意这些通知,因为可视化工具目标仅在调试会话处于活动状态且调试目标暂停时可用。 当可视化工具目标不可用时,对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 示例以查看这些技术在操作中。