Udostępnij za pomocą


Tworzenie wizualizatorów debugera programu Visual Studio

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 debuggera są dostępne z DataTip, który pojawia się po najechaniu kursorem na zmienną, lub w oknach Autos, Locals i Watch:.

Zrzut ekranu przedstawiający wizualizatory debugera w oknie podglądu.

Wprowadzenie

Postępuj zgodnie z sekcją Tworzenie projektu rozszerzenia w sekcji Wprowadzenie.

Następnie dodaj klasę rozszerzającą DebuggerVisualizerProvider i zastosuj VisualStudioContribution do niej atrybut:

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

Poprzedni kod definiuje nowy wizualizator debugera, który ma zastosowanie do obiektów typu string:

  • Właściwość DebuggerVisualizerProviderConfiguration definiuje nazwę wyświetlaną wizualizatora i obsługiwany typ platformy .NET.
  • Metoda CreateVisualizerAsync jest wywoływana przez program Visual Studio, gdy użytkownik żąda wyświetlenia wizualizatora debugera dla określonej wartości. CreateVisualizerAsync VisualizerTarget używa obiektu VisualizerTarget, aby pobrać wartość do wizualizacji i przekazać ją do niestandardowej kontroli użytkownika zdalnej (zobacz dokumentację zdalnego interfejsu użytkownika). Następnie zostanie zwrócona zdalna kontrola użytkownika i zostanie wyświetlona w oknie podręcznym w programie Visual Studio.

Celowanie w wiele typów

Właściwość konfiguracji umożliwia wizualizatorowi określanie wielu typów, gdy jest to wygodne. Doskonałym przykładem jest wizualizator zestawu danych DataSet , który obsługuje wizualizację DataSetobiektów , DataTable, DataViewi DataViewManager . Ta funkcja ułatwia opracowywanie rozszerzeń, ponieważ podobne typy mogą współdzielić ten sam interfejs użytkownika, wyświetlać modele i źródło obiektów wizualizatora.

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

Źródło obiektu wizualizatora

Źródło obiektu wizualizatora to klasa platformy .NET ładowana przez debuger w procesie debugowania. Wizualizator debugera może pobierać dane ze źródła obiektu wizualizatora przy użyciu metod udostępnianych przez VisualizerTarget.ObjectSource.

Domyślne źródło obiektu wizualizatora umożliwia wizualizatorom debugera pobieranie wartości obiektu do wizualizacji przez wywołanie RequestDataAsync<T>(JsonSerializer?, CancellationToken) metody . Domyślne źródło obiektu wizualizatora używa pliku Newtonsoft.Json do serializacji wartości, a biblioteki VisualStudio.Extensibility używają również pliku Newtonsoft.Json do deserializacji. Alternatywnie możesz użyć RequestDataAsync(CancellationToken), aby pobrać serializowaną wartość jako JToken.

Jeśli chcesz zwizualizować typ .NET, który jest natywnie obsługiwany przez Newtonsoft.Json, lub chcesz zwizualizować własny typ i możesz go serializować, poprzednie instrukcje są wystarczające do utworzenia prostego wizualizatora debugowania. Przeczytaj, czy chcesz obsługiwać bardziej złożone typy lub używać bardziej zaawansowanych funkcji.

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

Jeśli typ do wizualizacji nie może być automatycznie serializowany przez plik Newtonsoft.Json, możesz utworzyć niestandardowe źródło obiektu wizualizatora w celu obsługi serializacji.

  • Utwórz nowy projekt biblioteki klas platformy .NET przeznaczony dla elementu netstandard2.0. W razie potrzeby można skierować bardziej szczegółową wersję programu .NET Framework lub .NET (na przykład net472 lub net6.0), aby serializować obiekt do wizualizacji.
  • Dodaj odwołanie do pakietu w wersji 17.6 lub nowszej DebuggerVisualizers.
  • Dodaj klasę rozszerzającą VisualizerObjectSource i przesłoń GetData, zapisując zserializowaną wartość target do strumienia 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
        ...
    }
}

Używanie niestandardowej serializacji

Za pomocą metody VisualizerObjectSource.SerializeAsJson można serializować obiekt przy użyciu biblioteki Newtonsoft.Json do Stream bez dodawania odwołania do biblioteki Newtonsoft.Json do swojej biblioteki. Wywołanie SerializeAsJson spowoduje, że do debugowanego procesu zostanie załadowana przez mechanizm refleksji wersja zestawu Newtonsoft.Json.

Jeśli musisz odwołać się do pliku Newtonsoft.Json, należy użyć tej samej wersji, do Microsoft.VisualStudio.Extensibility.Sdk której odwołuje się pakiet, ale zaleca się używanie DataContract atrybutów i DataMember do obsługi serializacji obiektów zamiast polegać na typach Newtonsoft.Json.

Alternatywnie, możesz zaimplementować własną niestandardową serializację (na przykład serializację binarną), zapisując bezpośrednio do outgoingData.

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

Zmodyfikuj plik rozszerzenia .csproj dodając element ProjectReference do projektu biblioteki źródłowej obiektów wizualizatora, co gwarantuje, że biblioteka źródłowa obiektów wizualizatora została skompilowana przed spakowaniem rozszerzenia.

Dodaj również element zawierający bibliotekę DLL źródłową obiektów wizualizatora do podfolderu netstandard2.0 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>

Alternatywnie możesz użyć podfolderów net4.6.2 lub netcoreapp , jeśli utworzono bibliotekę źródłową obiektu wizualizatora przeznaczonego dla platformy .NET Framework lub .NET. Możesz nawet uwzględnić wszystkie trzy podfoldery z różnymi wersjami biblioteki źródłowej obiektów wizualizatora, ale lepiej jest kierować tylko na netstandard2.0.

Należy spróbować zminimalizować liczbę zależności pliku DLL biblioteki źródłowej obiektu wizualizatora. Jeśli biblioteka źródłowa obiektów wizualizatora ma zależności inne niż Microsoft.VisualStudio.DebuggerVisualizers i inne biblioteki, które są już zagwarantowane do załadowania w trakcie debugowania procesu, upewnij się, że te pliki DLL są również umieszczone w tym samym podfolderze co plik DLL biblioteki źródłowej obiektów wizualizatora.

Zaktualizować dostawcę wizualizatora debugera, aby używać niestandardowego obiektu źródłowego wizualizatora.

Następnie możesz zaktualizować konfigurację DebuggerVisualizerProvider , aby odwołać się do źródła obiektu niestandardowego wizualizatora:

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

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

Jeśli pobieranie danych ze źródła obiektu wizualizatora nie może być wykonywane za pomocą pojedynczego wywołania bez parametrów metody RequestDataAsync, można zamiast tego wykonać bardziej złożoną wymianę komunikatów ze źródłem obiektu wizualizatora, wywołując RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) wiele razy i wysyłając różne komunikaty do źródła obiektu wizualizatora. Zarówno komunikat, jak i odpowiedź są serializowane przez infrastrukturę VisualStudio.Extensibility przy użyciu pliku Newtonsoft.Json. Inne przesłonięcia RequestDataAsync umożliwiają wykorzystanie JToken obiektów lub implementowanie niestandardowej serializacji i deserializacji.

Można zaimplementować dowolny niestandardowy protokół przy użyciu różnych komunikatów w celu pobrania informacji ze źródła obiektu wizualizatora. Najczęstszym przypadkiem użycia tej funkcji jest podzielenie pobierania potencjalnie dużego obiektu na wiele wywołań, aby uniknąć RequestDataAsync przekroczenia limitu czasu.

Jest to przykład sposobu pozyskiwania zawartości potencjalnie dużej kolekcji po jednym elemencie naraz:

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

Powyższy kod używa prostego indeksu jako wiadomości do wywołań RequestDataAsync. Odpowiedni kod źródłowy obiektu wizualizatora zastąpi metodę TransferData (zamiast 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
        ...
    }
}

Źródło obiektu wizualizatora powyżej wykorzystuje metodę VisualizerObjectSource.DeserializeFromJson do deserializacji komunikatu wysyłanego przez dostawcę wizualizatora z usługi incomingData.

Podczas implementacji dostawcy wizualizatora debugera, który przeprowadza złożoną interakcję komunikatów ze źródłem obiektu wizualizatora, zwykle lepiej jest przekazać VisualizerTarget do RemoteUserControl wizualizatora, aby wymiana komunikatów mogła odbywać się asynchronicznie w trakcie ładowania kontrolki. Przekazywanie elementu VisualizerTarget umożliwia również wysyłanie komunikatów do źródła obiektu wizualizatora w celu pobrania danych na podstawie interakcji użytkownika z interfejsem użytkownika wizualizatora.

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

Otwieranie wizualizatorów jako okna narzędzi

Domyślnie wszystkie rozszerzenia wizualizatora debugera są otwierane jako modalne okna dialogowe na pierwszym planie programu Visual Studio. W związku z tym, jeśli użytkownik chce nadal korzystać ze środowiska IDE, wizualizator musi zostać zamknięty. Jeśli właściwość Style jest ustawiona na ToolWindow wewnątrz DebuggerVisualizerProviderConfiguration, wizualizator zostanie otwarty jako okno narzędziowe niemodalne, które może pozostać otwarte na resztę sesji debugowania. Jeśli nie zostanie zadeklarowany żaden styl, zostanie użyta wartość ModalDialog domyślna.

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

Za każdym razem, gdy wizualizator zdecyduje się otworzyć jako ToolWindow, należy zasubskrybować zdarzenie StateChanged komponentu VisualizerTarget. Gdy wizualizator zostanie otwarty jako okno narzędziowe, nie zablokuje użytkownikowi wznawiania zatrzymanej sesji debugowania. Tak więc wyżej wymienione zdarzenie zostanie wyzwolone przez debuger za każdym razem, gdy stan obiektu docelowego debugowania ulegnie zmianie. Autorzy rozszerzeń wizualizatora powinni zwrócić szczególną uwagę na te powiadomienia, ponieważ obiekt docelowy wizualizatora jest dostępny tylko wtedy, gdy sesja debugowania jest aktywna, a obiekt docelowy debugowania jest wstrzymany. Gdy obiekt docelowy wizualizatora jest niedostępny, wywołania metod ObjectSource zakończą się niepowodzeniem z powodu 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");
            }
        }
    }
}

Powiadomienie Available zostanie odebrane po utworzeniu RemoteUserControl i tuż przed jego udostępnieniem w nowo utworzonym oknie narzędzia wizualizatora. Tak długo, jak wizualizator pozostaje otwarty, inne VisualizerTargetStateNotification wartości mogą być odbierane za każdym razem, gdy obiekt docelowy debugowania zmieni jego stan. Powiadomienie ValueUpdated służy do wskazania, że ostatnie wyrażenie otwarte przez wizualizator zostało pomyślnie ponownie ocenione w punkcie, w którym debuger się zatrzymał i powinno zostać odświeżone przez interfejs użytkownika. Z drugiej strony, gdy obiekt docelowy debugowania zostanie wznowiony lub nie można ponownie ocenić wyrażenia po zatrzymaniu, Unavailable powiadomienie zostanie odebrane.

Aktualizowanie zwizualizowanej wartości obiektu

Jeśli VisualizerTarget.IsTargetReplaceable to prawda, wizualizator debugera może użyć ReplaceTargetObjectAsync metody w celu zaktualizowania wartości wizualizowanego obiektu w procesie debugowanym.

Źródło obiektu wizualizatora musi zastąpić metodę 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;
}

Wypróbuj przykład, RegexMatchDebugVisualizer aby zobaczyć te techniki w działaniu.