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 debugera są dostępne z poziomu etykietki danych wyświetlanej po umieszczeniu wskaźnika myszy na zmiennej lub w oknach Autos, Locals i Watch:

Screenshot of debugger visualizers in the watch window.

Rozpocznij

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. CreateVisualizerAsyncVisualizerTarget używa obiektu , aby pobrać wartość do wizualizacji i przekazuje ją do niestandardowego zdalnego sterowania użytkownika (odwołuje się do dokumentacji 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.

Określanie wartości docelowych dla wielu 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 uwidocznionych przez VisualizerTarget.ObjectSourceprogram .

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) polecenia , aby pobrać serializowaną wartość jako JToken.

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

Używanie niestandardowego źródła obiektu wizualizatora

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 DebuggerVisualizers wersji 17.6 lub nowszej.
  • Dodaj klasę rozszerzającą VisualizerObjectSource i przesłonięc GetData zapis zserializowanej wartości 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ą VisualizerObjectSource.SerializeAsJson metody można serializować obiekt przy użyciu pliku Newtonsoft.Json do Stream elementu bez dodawania odwołania do pliku Newtonsoft.Json do biblioteki. Wywołanie spowoduje załadowanie SerializeAsJson przez odbicie wersji zestawu Newtonsoft.Json do debugowanego procesu.

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żna zaimplementować własną niestandardową serializacji (na przykład serializacji binarnej) zapisywanie bezpośrednio w pliku 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ę Content DLL biblioteki źródłowej obiektów wizualizatora netstandard2.0 do podfolderu 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żna nawet uwzględnić wszystkie trzy podfoldery z różnymi wersjami biblioteki źródłowej obiektów wizualizatora, ale lepiej jest kierować tylko obiekty docelowe netstandard2.0 .

Należy spróbować zminimalizować liczbę zależności biblioteki DLL biblioteki źródłowej obiektów wizualizatora. Jeśli biblioteka źródłowa obiektów wizualizatora ma zależności inne niż Microsoft.VisualStudio.DebuggerVisualizers i biblioteki, które są już gwarantowane do załadowania w trakcie debugowania procesu, pamiętaj, aby uwzględnić te pliki DLL w tym samym podfolderze co biblioteka dll biblioteki źródłowej wizualizatora.

Aktualizowanie dostawcy wizualizatora debugera w celu używania niestandardowego źródła obiektu 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 RequestDataAsyncbez parametrów do metody , 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ą używanie 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. Najbardziej typowym przypadkiem użycia tej funkcji jest przerwanie pobierania potencjalnie dużego obiektu na wiele wywołań, aby uniknąć RequestDataAsync przekroczenia limitu czasu.

Jest to przykład sposobu pobierania zawartości potencjalnie dużej kolekcji jeden element jednocześnie:

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 komunikatu RequestDataAsync dla wywołań. 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 implementowania dostawcy wizualizatora debugera, który wykonuje złożoną interakcję komunikatu ze źródłem obiektu wizualizatora, zwykle lepiej jest przekazać VisualizerTarget element do wizualizatora RemoteUserControl , aby wymiana komunikatów mogła wystąpić asynchronicznie podczas ł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 Style jednak właściwość jest ustawiona na ToolWindowDebuggerVisualizerProviderConfiguration w właściwości, wizualizator zostanie otwarty jako okno narzędzia niemodalne, które może pozostać otwarte podczas pozostałej części 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 ToolWindowelement , należy zasubskrybować zdarzenie StateChanged zdarzenia VisualizerTarget. Gdy wizualizator zostanie otwarty jako okno narzędzi, nie zablokuje użytkownikowi wyrejestrowania 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 ObjectSource metod nie powiedzie się z elementem 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, gdzie debuger zatrzymał się i powinien zostać odświeżony 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.