Freigeben über


Erstellen von Visual Studio-Debuggervisualisierungen

Debugger-Visualizer sind ein Visual Studio-Feature, das während einer Debug-Sitzung eine benutzerdefinierte Visualisierung für Variablen oder Objekte eines bestimmten .NET-Typs bereitstellt.

Auf Debug-Visualisierungen kann über die Dateninfo zugegriffen werden, die angezeigt wird, wenn Sie mit der Maus über eine Variable fahren, oder aus den Fenstern Autos, Locals und Watch.

Screenshot der Debuggervisualisierer im Überwachungsfenster.

Get started

Folgen Sie dem Abschnitt "Erstellen des Erweiterungsprojekts " im Abschnitt "Erste Schritte".

Fügen Sie dann eine Klasse hinzu, die DebuggerVisualizerProvider erweitert, und wenden Sie das Attribut VisualStudioContribution darauf an:

/// <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);
    }
}

Der vorherige Code definiert eine neue Debuggerschnellansicht, die für Objekte vom Typ stringgilt:

  • Die DebuggerVisualizerProviderConfiguration Eigenschaft definiert den Anzeigenamen der Visualisierung und den unterstützten .NET-Typ.
  • Die CreateVisualizerAsync Methode wird von Visual Studio aufgerufen, wenn der Benutzer die Anzeige der Debuggerschnellansicht für einen bestimmten Wert anfordert. CreateVisualizerAsync verwendet das VisualizerTarget Objekt zum Abrufen des zu visualisierenden Werts und übergibt ihn an ein benutzerdefiniertes Remotebenutzersteuerelement (referenzieren Sie die Remote-UI-Dokumentation ). Die Remotebenutzersteuerung wird dann zurückgegeben und in einem Popupfenster in Visual Studio angezeigt.

Adressierung mehrerer Typen

Die Konfigurationseigenschaft ermöglicht es dem Visualizer, mehrere Typen zu verwenden, wenn dies praktisch ist. Ein perfektes Beispiel hierfür ist die DataSet Visualizer, die die Visualisierung von DataSet, , DataTableund DataViewDataViewManager Objekten unterstützt. Diese Funktion erleichtert die Erweiterungsentwicklung, da ähnliche Typen dieselbe Benutzeroberfläche, Ansichtsmodelle und Visualisiererobjektquelle gemeinsam nutzen können.

    /// <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)
    {
        ...
    }

Die Visualr-Objektquelle

Die Visualr-Objektquelle ist eine .NET-Klasse, die vom Debugger beim Debuggen geladen wird. Die Debugger-Visualizer kann Daten aus der Visualisierer-Objektquelle mithilfe der Methoden abrufen, die von VisualizerTarget.ObjectSource bereitgestellt werden.

Die Standard-Visualizer-Objektquelle ermöglicht Debugger-Visualisierern, den Wert des Objekts durch Aufrufen der RequestDataAsync<T>(JsonSerializer?, CancellationToken)-Methode abzurufen, das visualisiert werden soll. Die standardmäßige Visualisierer-Objektquelle verwendet Newtonsoft.Json, um den Wert zu serialisieren, und die VisualStudio.Extensibility-Bibliotheken verwenden ebenfalls Newtonsoft.Json für die Deserialisierung. Alternativ können Sie RequestDataAsync(CancellationToken) verwenden, um den serialisierten Wert als JToken abzurufen.

Wenn Sie einen .NET-Typ visualisieren möchten, der nativ von Newtonsoft.Json unterstützt wird oder Sie Ihren eigenen Typ visualisieren möchten und sie serialisierbar machen können, reichen die vorherigen Anweisungen aus, um eine einfache Debuggerschnellansicht zu erstellen. Lesen Sie weiter, wenn Sie komplexere Typen unterstützen oder erweiterte Features verwenden möchten.

Verwenden einer benutzerdefinierten Visualisiererobjektquelle

Wenn der zu visualisierende Typ nicht automatisch von Newtonsoft.Json serialisiert werden kann, können Sie eine benutzerdefinierte Visualisierungsobjektquelle erstellen, um die Serialisierung zu verarbeiten.

  • Erstellen Sie ein neues .NET-Klassenbibliotheksprojekt mit dem Ziel netstandard2.0. Sie können auf eine spezifischere Version von .NET Framework oder .NET abzielen (z. B. net472 oder net6.0), wenn es notwendig ist, das zu visualisierende Objekt zu serialisieren.
  • Fügen Sie einen Paketverweis auf DebuggerVisualizers Version 17.6 oder höher hinzu.
  • Fügen Sie eine Klasse hinzu, die VisualizerObjectSource erweitert und überschreiben Sie GetData, damit der serialisierte Wert von target in den outgoingData-Datenstrom geschrieben wird.
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
        ...
    }
}

Verwenden der benutzerdefinierten Serialisierung

Sie können die VisualizerObjectSource.SerializeAsJson-Methode verwenden, um ein Objekt mithilfe von Newtonsoft.Json in ein Stream zu serialisieren, ohne einen Verweis auf Newtonsoft.Json zu Ihrer Bibliothek hinzuzufügen. Durch das Aufrufen von SerializeAsJson wird eine Version der Newtonsoft.Json-Assembly per Reflection in den zu debuggenden Prozess geladen.

Wenn Sie auf Newtonsoft.Json verweisen müssen, sollten Sie dieselbe Version verwenden, auf die vom Microsoft.VisualStudio.Extensibility.Sdk-Paket verwiesen wird. Es ist jedoch vorzuziehen, DataContract- und DataMember-Attribute zur Unterstützung der Objektserialisierung zu verwenden, anstatt sich auf die Newtonsoft.Json-Typen zu verlassen.

Alternativ können Sie auch Ihre eigene benutzerdefinierte Serialisierung (z. B. binäre Serialisierung) direkt in outgoingData.

Fügen Sie die Visualizer-Objektquelle-DLL zur Erweiterung hinzu

Ändern Sie die Datei der Erweiterung .csproj , indem Sie ein ProjectReference zum Visualizer-Objektquellenbibliotheksprojekt hinzufügen, wodurch sichergestellt wird, dass die Visualizer-Objektquellenbibliothek erstellt wird, bevor die Erweiterung gepackt wird.

Fügen Sie außerdem ein Content Element einschließlich der DLL der Visualr-Objektquellbibliothek in den netstandard2.0 Unterordner der Erweiterung ein.

  <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>

Alternativ können Sie die net4.6.2- oder netcoreapp-Unterordner verwenden, wenn Sie die Visualizer-Objektquellbibliothek für .NET Framework oder .NET erstellt haben. Sie können sogar alle drei Unterordner mit unterschiedlichen Versionen der Visualizer-Objektquellbibliothek einschließen, aber es ist besser, nur netstandard2.0 als Ziel anzugeben.

Sie sollten versuchen, die Anzahl der Abhängigkeiten der Visualr-Objektquellbibliothek-DLL zu minimieren. Wenn ihre Visualr-Objektquellbibliothek andere Abhängigkeiten als Microsoft.VisualStudio.DebuggerVisualizers und Bibliotheken aufweist, die bereits garantiert im Debugvorgang geladen werden, stellen Sie sicher, dass diese DLL-Dateien auch in denselben Unterordner wie die Visualr-Objektquellbibliothek DLL eingeschlossen werden.

Aktualisieren Sie den Anbieter der Debugger-Visualisierung, um die benutzerdefinierte Visualisiererobjektquelle zu verwenden.

Anschließend können Sie Ihre DebuggerVisualizerProvider Konfiguration aktualisieren, um auf die benutzerdefinierte Visualr-Objektquelle zu verweisen:

    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);
    }

Arbeiten mit großen und komplexen Objekten

Wenn der Abruf von Daten aus der Visualr-Objektquelle nicht mit einem einzigen parameterlosen Aufruf RequestDataAsyncerfolgen kann, können Sie stattdessen einen komplexeren Nachrichtenaustausch mit der Visualisiererobjektquelle durchführen, indem Sie mehrere Male aufrufen RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) und verschiedene Nachrichten an die Visualr-Objektquelle senden. Sowohl die Nachricht als auch die Antwort werden von der VisualStudio.Extensibility-Infrastruktur mithilfe von Newtonsoft.Json serialisiert. Andere Überschreibungen von RequestDataAsync erlauben es Ihnen, JToken Objekte zu verwenden oder benutzerdefinierte Serialisierung und Deserialisierung zu implementieren.

Sie können jedes benutzerdefinierte Protokoll mithilfe verschiedener Meldungen implementieren, um Informationen aus der Visualisiererobjektquelle abzurufen. Der häufigste Anwendungsfall für dieses Feature besteht darin, den Abruf eines potenziell großen Objekts in mehrere Aufrufe aufzubrechen, um einen Timeout zu vermeiden RequestDataAsync .

Dies ist ein Beispiel dafür, wie Sie den Inhalt einer potenziell großen Sammlung Element für Element abrufen können.

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);
}

Der obige Code verwendet einen einfachen Index als Nachricht für die RequestDataAsync Aufrufe. Der entsprechende Visualr-Objektquellcode würde die TransferData Methode außer Kraft setzen (anstelle von 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
        ...
    }
}

Die oben genannte Visualr-Objektquelle nutzt die Methode, um die VisualizerObjectSource.DeserializeFromJson nachricht zu deserialisieren, die vom Visualizer-Anbieter incomingDatagesendet wurde.

Bei der Implementierung eines Debugger-Visualizer-Anbieters, der komplexe Nachrichteninteraktionen mit der Visualizer-Objektquelle durchführt, empfiehlt es sich in der Regel, die VisualizerTarget an den Visualizer RemoteUserControl zu übergeben, damit der Nachrichtenaustausch asynchron erfolgen kann, während das Steuerelement geladen wird. Durch Das Übergeben des VisualizerTarget Objekts können Sie auch Nachrichten an die Visualisiererobjektquelle senden, um Daten basierend auf den Interaktionen des Benutzers mit der Benutzeroberfläche der Visualisierung abzurufen.

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
        ...
    }
    ...

Öffnen von Visualisierungen als Werkzeugfenster

Standardmäßig werden alle Debuggerschnellansichtserweiterungen als modale Dialogfenster im Vordergrund von Visual Studio geöffnet. Wenn der Benutzer also weiterhin mit der IDE interagieren möchte, muss die Visualisierung geschlossen werden. Wenn jedoch die Eigenschaft Style auf ToolWindow in der Eigenschaft DebuggerVisualizerProviderConfiguration festgelegt ist, wird der Visualizer als nicht-modales Toolfenster geöffnet, das für den Rest der Debug-Sitzung geöffnet bleiben kann. Wenn keine Formatvorlage deklariert wird, wird der Standardwert ModalDialog verwendet.

    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);
    }

Immer wenn ein Visualizer als ein ToolWindow geöffnet wird, muss er das StateChanged-Ereignis des VisualizerTarget abonnieren. Wenn eine Visualisierung als Toolfenster geöffnet wird, wird der Benutzer nicht daran gehindert, die Debugsitzung aufzuheben. Das oben genannte Ereignis wird also vom Debugger ausgelöst, wenn sich der Status des Debugziels ändert. Autoren der Visualizer-Erweiterung sollten diese Benachrichtigungen besonders berücksichtigen, da das Visualisiererziel nur verfügbar ist, wenn die Debugsitzung aktiv ist und das Debugziel angehalten wird. Wenn das Visualizer-Ziel nicht verfügbar ist, treten Aufrufe von ObjectSource-Methoden mit einem VisualizerTargetUnavailableException Fehler auf.

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");
            }
        }
    }
}

Die Available Benachrichtigung wird empfangen, nachdem die RemoteUserControl Benachrichtigung erstellt wurde und unmittelbar bevor sie im neu erstellten Visualisierungstoolfenster sichtbar gemacht wird. Solange der Visualizer geöffnet bleibt, können die anderen VisualizerTargetStateNotification Werte jedes Mal empfangen werden, wenn das Debugziel seinen Zustand ändert. Die ValueUpdated Benachrichtigung wird verwendet, um anzugeben, dass der letzte Ausdruck, der vom Visualizer geöffnet wurde, erfolgreich neu ausgewertet wurde, wo der Debugger angehalten wurde, und durch die Benutzeroberfläche aktualisiert werden sollte. Auf der anderen Seite, wenn das Debugziel fortgesetzt wird oder der Ausdruck nicht erneut ausgewertet werden kann, nachdem ein Stopp erfolgt ist, wird die Unavailable Benachrichtigung empfangen.

Aktualisieren des visualisierten Objektwerts

Wenn VisualizerTarget.IsTargetReplaceable wahr ist, kann der Debugger-Visualizer die ReplaceTargetObjectAsync-Methode verwenden, um den Wert des visualisierten Objekts im Entwurfsprozess zu aktualisieren.

Die Visualizer-Objektquelle muss die CreateReplacementObject Methode überschreiben.

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

Testen Sie das RegexMatchDebugVisualizer Beispiel, um diese Techniken in Aktion zu sehen.