Wizualizatory debuggera

Wizualizatory debugera to funkcja programu Visual Studio, która udostępnia niestandardową wizualizację zmiennych lub obiektów określonego typu platformy .NET podczas sesji debugowania. Wizualizatory debugera są dostępne z poziomu DataTip wyświetlanego po najechaniu kursorem na zmienną lub w oknach Autos, Locals i Watch.

Note

W tym artykule jest używany zestaw VisualStudio.Extensibility SDK. Aby uzyskać pomoc dotyczącą wyboru między programem VisualStudio.Extensibility i starszym zestawem VSSDK, zobacz Wybieranie odpowiedniego modelu rozszerzalności.

Zrzut ekranu przedstawiający wizualizatory debugera w oknie Obserwatora.

Pełny przykład

Kompletna implementacja znajduje się w przykładzie:

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace RegexMatchVisualizer;

using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;
using RegexMatchVisualizer.ObjectSource;

/// <summary>
/// Debugger visualizer provider class for <see cref="Match"/>.
/// </summary>
[VisualStudioContribution]
internal class RegexMatchDebuggerVisualizerProvider : DebuggerVisualizerProvider
{
    /// <inheritdoc/>
    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("%RegexMatchVisualizer.RegexMatchDebuggerVisualizerProvider.DisplayName%", typeof(Match))
    {
        VisualizerObjectSourceType = new("RegexMatchVisualizer.ObjectSource.RegexMatchObjectSource, RegexMatchObjectSource"),
    };

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

        return new RegexMatchVisualizerUserControl(regexMatch);
    }
}

Tip

Zobacz pełny przykład roboczy: RegexMatchDebugVisualizer ↗

Powiązane pojęcia:Zdalny interfejs użytkownika, wkłady i konfiguracje oraz Tworzenie pierwszego rozszerzenia

Zrozum przykład

Wprowadzenie

Aby rozpocząć, utwórz projekt rozszerzenia VisualStudio.Extensibility, postępując zgodnie z instrukcjami w artykule Tworzenie pierwszego rozszerzenia.

Każde rozszerzenie rozpoczyna się od Extensionpunktu wejścia definiującego ExtensionConfigurationmetadane:

[VisualStudioContribution]
internal class MyExtension : Extension
{
    public override ExtensionConfiguration ExtensionConfiguration => new()
    {
        Metadata = new(
            id: "MyExtension.01234567",
            version: this.ExtensionAssemblyVersion,
            publisherName: "Contoso",
            displayName: "My debugger visualizer extension"),
    };
}

Utwórz dostawcę wizualizatora debugera

Dodaj klasę, która rozszerza DebuggerVisualizerProvider ↗, i zastosuj do niej atrybut VisualStudioContribution ↗:

[VisualStudioContribution]
internal class StringDebuggerVisualizerProvider : DebuggerVisualizerProvider
{
    public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new(
        "My string visualizer",
        typeof(string));

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

        return new MyStringVisualizerControl(targetObjectValue);
    }
}

Ten kod definiuje nowy wizualizator debugera dla string wartości:

  • DebuggerVisualizerProviderConfigurationdefiniuje nazwę wyświetlaną wizualizatora i obsługiwany typ .NET.
  • CreateVisualizerAsync jest wywoływany po otwarciu wizualizatora przez użytkownika. Używa VisualizerTarget ↗ do pobierania danych i zwraca kontrolkę zdalnego interfejsu użytkownika, którą program Visual Studio wyświetla w oknie podręcznym.

Ten sam obiekt konfiguracji to miejsce, w którym deklarujesz szczegóły, takie jak typy docelowe, nazwy wyświetlane wizualizatora, VisualizerObjectSourceType oraz to, czy wizualizator otwiera się jako modalne okno dialogowe, czy jako niemodalne okno narzędzia.

Wybierz wiele typów

Właściwość konfiguracji może być skierowana do wielu typów, gdy warto współużytkować ten sam interfejs użytkownika, modele widoku i źródło obiektu wizualizatora. Typowym przykładem jest tabelaryczny wizualizator obsługujący DataSet, DataTable, DataView i DataViewManager.

public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new(
    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)));

Praca z obiektem źródłowym wizualizatora

Obiekt źródłowy wizualizatora jest klasą .NET, którą debuger ładuje do debugowanego procesu. Wizualizator debugera pobiera z niego dane za pośrednictwem elementu VisualizerTarget.ObjectSource ↗ .

Domyślne źródło obiektu wizualizatora obsługuje:

Domyślna implementacja używa Newtonsoft.Json ↗ do serializacji. Jeśli typ docelowy jest już możliwy do serializacji lub jeśli można sprawić, by własny typ był serializowalny, ten przepływ domyślny jest często wystarczający.

Użycie niestandardowego źródła obiektu wizualizacji

Jeśli typ do wizualizacji nie może być automatycznie serializowany przez Newtonsoft.Json, utwórz niestandardowe źródło obiektu wizualizatora.

  1. Utwórz nową bibliotekę klas ukierunkowaną na netstandard2.0. Jeśli jest to wymagane, możesz określić bardziej szczegółową platformę, taką jak net472 lub net6.0.
  2. Dodaj odwołanie do pakietu Microsoft.VisualStudio.DebuggerVisualizers ↗ w wersji 17.6 lub nowszej.
  3. Utwórz klasę, która rozszerza VisualizerObjectSource ↗ i przesłania element GetData.
public class MyObjectSource : VisualizerObjectSource
{
    public override void GetData(object target, Stream outgoingData)
    {
        MySerializableType result = Convert(target);
        SerializeAsJson(outgoingData, result);
    }

    private static MySerializableType Convert(object target)
    {
        // Convert the debuggee object into a serializable type.
        throw new NotImplementedException();
    }
}

Jeśli korzystasz z przykładu Regex, RegexMatchObjectSource konwertuje Match do postaci możliwej RegexMatch do serializacji, zanim dostawca zażąda go.

Używanie niestandardowej serializacji

Użyj VisualizerObjectSource.SerializeAsJson, aby serializować obiekty przy użyciu Newtonsoft.Json bez dodawania bezpośredniej zależności Newtonsoft.Json do biblioteki object-source. Metoda ładuje bibliotekę Newtonsoft.Json w procesie debuggee za pomocą mechanizmu refleksji.

Jeśli zdecydujesz się odwoływać bezpośrednio do Newtonsoft.Json, użyj tej samej wersji, do której odwołuje się Microsoft.VisualStudio.Extensibility.Sdk. W większości przypadków lepiej używać atrybutów DataMember i DataContract dla typów udostępnionych zamiast polegać na typach DataContract specyficznych dla Newtonsoft.Json.

Możesz również zaimplementować własny format serializacji i zapisać bezpośrednio do outgoingData.

Dodawanie źródłowej biblioteki DLL obiektu wizualizatora do rozszerzenia

Dodaj element ProjectReference, aby projekt object-source został skompilowany, zanim rozszerzenie zostanie spakowane, i dodaj element Content, aby biblioteka DLL została skopiowana do danych wyjściowych rozszerzenia.

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

Można również użyć podfolderów net462 lub netcoreapp, jeśli biblioteka źródłowa obiektów jest przeznaczona dla tych platform docelowych, ale netstandard2.0 jest preferowana, jeśli to możliwe.

Zachowaj graf zależności biblioteki źródeł obiektów tak mały, jak to możliwe. Jeśli zależy to od innych bibliotek DLL, których debugowany proces jeszcze nie załadował, uwzględnij te biblioteki DLL w tym samym podfolderze rozszerzenia.

Skonfiguruj dostawcę, aby używał niestandardowego źródła obiektów

Po spakowaniu biblioteki DLL źródła obiektu zaktualizuj DebuggerVisualizerProviderConfiguration, aby wskazywał na nią:

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

Jeśli celowo unikasz odwołania do zestawu z rozszerzenia do projektu object-source, skonfiguruj VisualizerObjectSourceType przy użyciu nazwy kwalifikowanej przez zestaw zamiast typeof(...). Przykład Regex używa tego wzorca.

Praca z dużymi i złożonymi obiektami

Jeśli pobieranie wszystkich danych w jednym wywołaniu RequestDataAsync bez parametrów nie jest praktyczne, użyj polecenia RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) ↗ do wymiany komunikatów ze źródłem obiektu.

Ten wzorzec jest przydatny w przypadku dużych kolekcji, ponieważ każde RequestDataAsync wywołanie ma pięciosekundowy limit czasu. Typowym podejściem jest żądanie wpisów pojedynczo:

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

Po stronie źródła obiektów nadpisz GetData elementem TransferData:

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

DeserializeFromJson Odczytuje komunikat wysłany przez dostawcę za pośrednictwem incomingData

Gdy wizualizator wykonuje bardziej złożone wymiany komunikatów, często lepiej jest przekazać odwołanie VisualizerTarget ↗ do zdalnej kontrolki użytkownika, aby mogła asynchronicznie żądać danych podczas ładowania kontrolki lub później w odpowiedzi na interakcję użytkownika:

public override Task<IRemoteUserControl> CreateVisualizerAsync(
    VisualizerTarget visualizerTarget,
    CancellationToken cancellationToken)
{
    return Task.FromResult<IRemoteUserControl>(new MyVisualizerUserControl(visualizerTarget));
}

Otwórz wizualizatory jako okna narzędzi

Domyślnie wizualizatory debugera otwierają się jako modalne okna dialogowe na pierwszym planie Visual Studio. Jeśli użytkownik chce kontynuować interakcję ze środowiskiem IDE, najpierw musi zamknąć okno modalne wizualizatora.

Jeśli ustawisz Style = VisualizerStyle.ToolWindow, wizualizator zostanie otwarty jako niemodalne okno narzędziowe i może pozostać otwarty przez resztę sesji debugowania. Jeśli nie ustawisz Styleparametru , wartość domyślna to ModalDialog.

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

Gdy wizualizator otworzy się jako okno narzędzia, musi nasłuchiwać zdarzenia VisualizerTarget.Changed. Okna narzędzi nie blokują użytkownikowi wznowienia wykonywania, więc debuger zgłasza powiadomienia za każdym razem, gdy cel debugowania ulegnie zmianie. Gdy element docelowy jest niedostępny, wywołania do ObjectSource kończą się niepowodzeniem z błędem 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;
            this.visualizerTarget.Changed += 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 this.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 jest wywoływany po utworzeniu elementu RemoteUserControl i tuż przed wyświetleniem okna narzędzia w programie Visual Studio. ValueUpdated wskazuje, że wyrażenie zostało pomyślnie ponownie ocenione w bieżącym stanie przerwania, a interfejs użytkownika powinien zostać odświeżony. Unavailable wskazuje, że wykonanie zostało wznowione lub że nie można było ponownie ocenić wyrażenia po zatrzymaniu wykonywania.

Aktualizowanie zwizualizowanej wartości obiektu

Jeśli VisualizerTarget.IsTargetReplaceable ↗ ma wartość true, wizualizator może zaktualizować wartość debuggee, wywołując metodę ReplaceTargetObjectAsync ↗.

await visualizerTarget.ObjectSource.ReplaceTargetObjectAsync(
    newValue,
    jsonSerializer: null,
    cancellationToken);

Źródło obiektu wizualizatora musi przesłonić CreateReplacementObject:

public override object CreateReplacementObject(object target, Stream incomingData)
{
    // Read the replacement value from incomingData.
    return newValue;
}

Porady

✅ DO

  • Umieść metadane rozszerzenia w ExtensionConfiguration, a ustawienia współtworzenia w DebuggerVisualizerProviderConfiguration.
  • Użyj polecenia netstandard2.0 dla projektu object-source, chyba że masz określony powód, aby zastosować inny framework.
  • Używaj DataContractudostępnionych typów danychDataMember.
  • Spakuj bibliotekę DLL źródła obiektu i wszystkie wymagane zależności razem z rozszerzeniem.
  • Użyj stronicowanych RequestDataAsync wywołań dla dużych lub kosztownych obiektów.
  • Obsługuj VisualizerTarget.Changed podczas otwierania jako okno narzędziowe.

❌ NIE RÓB

  • Nie odwołuj się do pakietów VisualStudio.Extensibility z projektu object-source.
  • Nie zakładaj, że każdy typ może być serializowany przez domyślne źródło obiektów.
  • Nie pozostawiaj wizualizatora okna narzędziowego nieświadomego powiadomień Unavailable.
  • Nie zapomnij, że wywołania RequestDataAsync mogą przekroczyć limit czasu po pięciu sekundach.

🔧 Rozwiązywanie problemów

Objaw Co należy sprawdzić?
Wizualizator się nie pojawia Sprawdź, czy klasa dostawcy ma [VisualStudioContribution] i czy DebuggerVisualizerProviderConfiguration wskazuje poprawny typ .NET.
RequestDataAsync kończy się niepowodzeniem lub przekracza limit czasu Przenieś pobieranie dużych danych do TransferData i podziel dane na strony w wielu wywołaniach.
Nie znaleziono źródła obiektu niestandardowego Upewnij się, że biblioteka DLL jest kopiowana do folderu rozszerzenia netstandard2.0 i VisualizerObjectSourceType wskazuje poprawną nazwę typu lub nazwę kwalifikowaną zestawem.
Wizualizator okna narzędzi powoduje wyjątek, gdy debugowanie jest kontynuowane Obsłuż VisualizerTargetStateNotification.Unavailable i unikaj wywołań ObjectSource, dopóki obiekt docelowy nie będzie ponownie dostępny.

Następne kroki