建立 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 類型。
  • 當使用者要求偵錯工具視覺化檢視顯示特定值時,Visual Studio 會叫用 CreateVisualizerAsync 方法。 CreateVisualizerAsync 使用 VisualizerTarget 物件來擷取要視覺化的值,並將其傳遞至自訂遠端使用者控制項 (參考遠端 UI 文件)。 然後會傳回遠端使用者控制項,並顯示在 Visual Studio 的彈出視窗中。

以多個類型為目標

組態屬性可讓視覺化檢視在方便時以多個類型為目標。 其中一個完美的範例是支援 DataSetDataTableDataView,和 DataViewManager 物件之視覺效果的 DataSet 視覺化檢視。 此功能可簡化擴充功能開發,因為類似的類型可以共用相同的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 版本 (例如,net472net6.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 將物件序列化為 Stream,而不需將 Newtonsoft.Json 的參考新增至您的程式庫。 叫用 SerializeAsJson 將會透過反射將 Newtonsoft.Json 組件的版本載入到正在偵錯的處理序中。

如果您需要參考 Newtonsoft.Json,您應該使用 Microsoft.VisualStudio.Extensibility.Sdk 套件所參考的相同版本,但最好使用 DataContractDataMember 屬性來支援物件序列化,而不是依賴 Newtonsoft.Json 類型。

或者,您可以實作您的自訂序列化 (例如二進位序列化) 直接寫入 outgoingData

將視覺化檢視物件來源 DLL 新增至擴充功能

修改擴充功能 .csproj 檔案,將 ProjectReference 新增至視覺化物件來源程式庫專案,可確保在封裝擴充功能之前組建視覺化檢視物件來源程式庫。

此外,將包含視覺化檢視物件來源程式庫 DLL 的 Content 項目新增至 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.DebuggerVisualizers 以外的相依性,並且已保證在正在偵錯的處理序中載入這些程式庫,請確保也將這些 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");
            }
        }
    }
}

RemoteUserControl 建立後且在新建立的視覺化檢視工具視窗中可見之前將收到 Available 通知。 只要視覺化檢視保持開啟狀態,每次偵錯目標變更其狀態時,就可以收到其他 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 範例以查看這些技術的實際應用。