Erstellen von Visual Studio-Debuggervisualisierungen

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

Auf Debuggerschnellansichten kann über die Dateninfo zugegriffen werden, die beim Daraufzeigen über eine Variable oder über die Fenster "Autos", "Lokal" und "Überwachung" angezeigt wird:

Screenshot of debugger visualizers in the watch window.

Erste Schritte

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

Fügen Sie dann eine Klasse hinzu, die das Attribut erweitert DebuggerVisualizerProvider , und wenden Sie es VisualStudioContribution 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 Debuggerschnellansicht kann Daten aus der Visualisiererobjektquelle mithilfe von Methoden abrufen, die von VisualizerTarget.ObjectSource.

Die Standardmäßige Visualisiererobjektquelle ermöglicht Debuggerschnellansichten das Abrufen des Werts des Objekts, das durch Aufrufen der RequestDataAsync<T>(JsonSerializer?, CancellationToken) Methode visualisiert werden soll. Die Standardmäßige Visual Visualr-Objektquelle verwendet Newtonsoft.Json , um den Wert zu serialisieren, und die VisualStudio.Extensibility-Bibliotheken verwenden auch Newtonsoft.Json für die Deserialisierung. Alternativ können RequestDataAsync(CancellationToken) Sie den serialisierten Wert als ein JToken.

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 für die Zielbestimmung netstandard2.0. Sie können auf eine spezifischere Version von .NET Framework oder .NET (z. B. oder net6.0) abzielen, net472 um 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 den serialisierten Wert des outgoingDatatarget Datenstroms erweitert VisualizerObjectSource und überschreibtGetData.
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 zu serialisieren Stream , ohne einen Verweis auf Newtonsoft.Json zu Ihrer Bibliothek hinzuzufügen. Durch Aufrufen SerializeAsJson wird eine Version der Newtonsoft.Json-Assembly über Spiegelung 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 empfiehlt DataContractDataMember sich jedoch, die Objekt serialisierung zu unterstützen, anstatt auf Newtonsoft.Json-Typen zu vertrauen.

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

Hinzufügen der Visualr-Objektquell-DLL zur Erweiterung

Ändern Sie die Erweiterungsdatei .csproj , die dem Visualr-Objektquellbibliotheksprojekt einfügt ProjectReference , wodurch sichergestellt wird, dass die Visualr-Objektquellbibliothek 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-Unterordner netcoreapp verwenden, wenn Sie die Visualr-Objektquellbibliothek für .NET Framework oder .NET erstellt haben. Sie können sogar alle drei Unterordner mit unterschiedlichen Versionen der Visualr-Objektquellbibliothek einschließen, aber es ist besser, nur als Ziel zu verwenden netstandard2.0 .

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 des Debuggerschnellansichtsanbieters für die Verwendung der benutzerdefinierten Visualisiererobjektquelle

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 Außerkraftsetzungen, mit denen RequestDataAsync Sie Objekte verwenden JToken oder benutzerdefinierte Serialisierung und Deserialisierung implementieren können.

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 jeweils ein 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 Debuggerschnellansichtsanbieters, der komplexe Nachrichteninteraktionen mit der Visualr-Objektquelle durchführt, empfiehlt es sich in der Regel, die VisualizerTarget Daten an die Visualisierung 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 Windows-Tool

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 die Style Eigenschaft jedoch in der DebuggerVisualizerProviderConfiguration Eigenschaft festgelegt ToolWindow ist, wird die Visualisierung als nicht modales Toolfenster geöffnet, das während der restlichen Debugsitzung erneut geöffnet werden kann Standard geöffnet werden 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);
    }

Jedes Mal, wenn ein Visualizer als eine ToolWindowgeöffnet wird, muss es das StateChanged-Ereignis des VisualizerTarget. Wenn eine Visualisierung als Toolfenster geöffnet wird, wird der Benutzer nicht daran gehindert, die Debugsitzung aufzuheben. Das folgende Erwähnung ed-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 VisualizerTargetUnavailableExceptionFehler 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 die Visualisierung wieder geöffnet ist Standard werden die anderen VisualizerTargetStateNotification Werte jedes Mal empfangen, wenn das Debugziel seinen Zustand ändert. Die ValueUpdated Benachrichtigung wird verwendet, um anzugeben, dass der letzte ausdruck, der von der Visualisierung geöffnet wurde, erfolgreich neu ausgewertet wurde, wo der Debugger beendet wurde und von der Benutzeroberfläche aktualisiert werden soll. Wenn das Debugziel fortgesetzt wird oder der Ausdruck nach dem Beenden nicht erneut ausgewertet werden kann, wird die Unavailable Benachrichtigung empfangen.

Aktualisieren des visualisierten Objektwerts

Ist VisualizerTarget.IsTargetReplaceable dies der Fall, kann die Debuggerschnellansicht die ReplaceTargetObjectAsync Methode verwenden, um den Wert des visualisierten Objekts im zu debuggenden Prozess zu aktualisieren.

Die Visualr-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.