Visualizzatori del debugger

I visualizzatori del debugger sono una funzionalità di Visual Studio che fornisce una visualizzazione personalizzata per variabili o oggetti di un tipo .NET specifico durante una sessione di debug. I visualizzatori del debugger sono accessibili dal DataTip visualizzato quando si passa il puntatore del mouse su una variabile o dalle finestre Variabili automatiche, Variabili locali e Controllo.

Note

Questo articolo usa VisualStudio.Extensibility SDK. Per informazioni sulla scelta tra VisualStudio.Extensibility e VSSDK legacy, vedere Scegliere il modello di estendibilità corretto.

Screenshot che mostra i visualizzatori del debugger nella finestra Controllo.

Esempio completo

L'implementazione completa è nell'esempio:

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

Vedere l'esempio funzionante completo: RegexMatchDebugVisualizer ↗

Concetti correlati:Interfaccia utente remota, contributi e configurazioni e Creare la prima estensione

Capire l’esempio

Get started

Per iniziare, creare un progetto di estensione VisualStudio.Extensibility seguendo l'articolo Creare la prima estensione.

Ogni estensione inizia con un Extension punto di ingresso che definisce ExtensionConfiguration i metadati:

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

Crea il provider del visualizzatore del debugger

Aggiungere una classe che estende DebuggerVisualizerProvider ↗ e applicare l'attributo VisualStudioContribution ↗ a esso:

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

Questo codice definisce un nuovo visualizzatore del debugger per string i valori:

  • DebuggerVisualizerProviderConfigurationdefinisce il nome visualizzatore e il tipo di .NET supportato.
  • CreateVisualizerAsync viene richiamato quando l'utente apre il visualizzatore. Usa VisualizerTarget per recuperare i dati e restituisce un controllo Remote UI che Visual Studio visualizza in una finestra popup.

Lo stesso oggetto di configurazione è dove si dichiarano dettagli quali i tipi di destinazione, i nomi visualizzati del visualizzatore, VisualizerObjectSourceTypee se il visualizzatore viene aperto come finestra di dialogo modale o come finestra degli strumenti non modale.

Più tipi di destinazione

La proprietà di configurazione può avere come destinazione più tipi quando è opportuno condividere la stessa interfaccia utente, i modelli di visualizzazione e l'origine oggetto del visualizzatore. Un esempio comune è un visualizzatore tabulare che supporta DataSet, DataView, DataTable e 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)));

Lavorare con l'origine dell'oggetto visualizzatore

L'oggetto di origine del visualizzatore è una classe .NET caricata dal debugger nel processo di cui è in corso il debug. Il visualizzatore del debugger recupera i dati da esso tramite VisualizerTarget.ObjectSource ↗ .

L'oggetto sorgente del visualizzatore predefinito supporta:

L'implementazione predefinita usa Newtonsoft.Json ↗ per la serializzazione. Se il tipo di destinazione è già serializzabile o se è possibile rendere serializzabile il proprio tipo, questo flusso predefinito è spesso sufficiente.

Usare un oggetto visualizzatore personalizzato come origine

Se il tipo da visualizzare non può essere serializzato automaticamente da Newtonsoft.Json, creare un'origine oggetto per un visualizzatore personalizzato.

  1. Creare una nuova libreria di classi destinata a netstandard2.0. Se necessario, è possibile specificare come destinazione un framework più specifico, net472 ad esempio o net6.0.
  2. Aggiungere un riferimento al pacchetto Microsoft.VisualStudio.DebuggerVisualizers ↗ versione 17.6 o successiva.
  3. Creare una classe che estende VisualizerObjectSource ↗ e sovrascrivi 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();
    }
}

Se si segue l'esempio Regex, RegexMatchObjectSource converte Match in una forma serializzabile RegexMatch prima che il provider lo richieda.

Usare la serializzazione personalizzata

Usare VisualizerObjectSource.SerializeAsJson per serializzare gli oggetti con Newtonsoft.Json senza aggiungere una dipendenza Newtonsoft.Json diretta alla libreria object-source. Il metodo carica l'assembly Newtonsoft.Json nel processo di debug tramite reflection.

Se si decide di fare riferimento direttamente a Newtonsoft.Json, usare la stessa versione a cui fa riferimento Microsoft.VisualStudio.Extensibility.Sdk. Nella maggior parte dei casi, è preferibile usare gli attributi DataContract e DataMember per i tipi condivisi anziché basarsi su tipi specifici di Newtonsoft.Json.

È anche possibile implementare il proprio formato di serializzazione e scrivere direttamente in outgoingData.

Aggiungere la DLL dell'origine oggetto del visualizzatore all'estensione

Aggiungere un ProjectReference oggetto in modo che il progetto object-source venga compilato prima della creazione del pacchetto dell'estensione e aggiungere un Content elemento in modo che la DLL venga copiata nell'output dell'estensione.

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

È anche possibile usare net462 o netcoreapp sottocartelle se la libreria di origine oggetto è destinata a tali framework, ma netstandard2.0 è preferibile quando possibile.

Mantenere il grafico delle dipendenze della libreria object-source il più piccolo possibile. Se dipende da altre DLL che il processo in debug non carica già, includere tali DLL nella stessa sottocartella dell'estensione.

Configurare il provider per l'uso dell'origine oggetto personalizzata

Dopo aver creato il pacchetto della DLL dell'origine oggetto, aggiorna DebuggerVisualizerProviderConfiguration in modo che punti a esso:

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

Se si evita intenzionalmente un riferimento all'assembly dall'estensione al progetto sorgente dell'oggetto, configurare VisualizerObjectSourceType con il nome completo dell'assembly anziché typeof(...). L'esempio Regex usa tale modello.

Usare oggetti di grandi dimensioni e complessi

Se il recupero di tutti i dati in una chiamata senza RequestDataAsync parametri non è pratico, usare RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) ↗ per scambiare messaggi con l'origine dell'oggetto.

Questo modello è utile per le raccolte di grandi dimensioni perché ogni chiamata RequestDataAsync ha un timeout di cinque secondi. Un approccio comune consiste nel richiedere voci una alla volta:

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

Sul lato origine oggetto eseguire l'override TransferData anziché 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);
        }
    }
}

DeserializeFromJson legge il messaggio inviato dal provider tramite incomingData.

Quando un visualizzatore esegue scambi di messaggi più complessi, spesso è preferibile passare questo VisualizerTarget ↗ al controllo utente remoto in modo che possa richiedere i dati in modo asincrono durante il caricamento del controllo o successivamente in risposta all'interazione dell'utente:

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

Aprire i visualizzatori come finestre degli strumenti

Per impostazione predefinita, i visualizzatori del debugger aprono finestre di dialogo modali in primo piano in Visual Studio. Se l'utente vuole continuare a interagire con l'IDE, deve prima chiudere il visualizzatore modale.

Se si imposta Style = VisualizerStyle.ToolWindow, il visualizzatore viene aperto come finestra degli strumenti non modale e può rimanere aperto per il resto della sessione di debug. Se non si imposta Style, il valore predefinito è ModalDialog.

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

Quando un visualizzatore viene aperto come finestra degli strumenti, deve registrarsi all'evento VisualizerTarget.Changed. I pannelli degli strumenti non impediscono all'utente di riprendere l'esecuzione, quindi il debugger genera notifiche ogni volta che la destinazione di debug cambia stato. Quando la destinazione non è disponibile, le chiamate a ObjectSource non riescono con 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");
            }
        }
    }
}

Availableviene generato dopo la creazione dell'oggetto RemoteUserControl e appena prima che Visual Studio visualizzi la finestra degli strumenti. ValueUpdated indica che l'espressione è stata rivalutata correttamente allo stato di interruzione corrente e che l'interfaccia utente deve essere aggiornata. Unavailable indica che l'esecuzione è ripresa o che non è stato possibile rivalutare l'espressione dopo l'arresto dell'esecuzione.

Aggiornare il valore dell'oggetto visualizzato

Se VisualizerTarget.IsTargetReplaceable ↗ è true, il visualizzatore può aggiornare il valore del debuggee chiamando ReplaceTargetObjectAsync ↗.

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

L'oggetto di origine del visualizzatore deve eseguire l'override di CreateReplacementObject:

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

Tips

✅ DO

  • Inserire i metadati dell'estensione in ExtensionConfiguration e le impostazioni di contributo in DebuggerVisualizerProviderConfiguration.
  • Usare netstandard2.0 per il progetto sorgente dell'oggetto, a meno che non si disponga di un motivo specifico per impostare come destinazione un altro framework.
  • Usare DataContract e DataMember sui tipi di dati condivisi.
  • Creare il pacchetto della DLL dell'origine oggetto ed eventuali dipendenze necessarie con l'estensione.
  • Usare chiamate paginate RequestDataAsync per oggetti di grandi dimensioni o costosi.
  • Gestisce VisualizerTarget.Changed quando viene aperto come finestra degli strumenti.

❌ DON'T

  • Non fare riferimento ai pacchetti VisualStudio.Extensibility dal progetto object-source.
  • Non presupporre che ogni tipo possa essere serializzato dall'origine oggetti predefinita.
  • Non lasciare un visualizzatore di finestre degli strumenti ignaro delle notifiche Unavailable.
  • Non dimenticare che RequestDataAsync le chiamate possono scadere dopo cinque secondi.

🔧 Risoluzione dei problemi

Sintomo Controlli da eseguire
Il visualizzatore non viene visualizzato Verificare che la classe del provider abbia [VisualStudioContribution] e che DebuggerVisualizerProviderConfiguration abbia come destinazione il tipo di .NET corretto.
RequestDataAsync fallisce o va in timeout Spostare il recupero di grandi quantità di dati in TransferData e suddividere i dati su più chiamate.
L'origine dell'oggetto personalizzato non viene trovata Verificare che la DLL venga copiata nella cartella dell'estensione netstandard2.0 e VisualizerObjectSourceType punti al tipo corretto o al nome qualificato dell'assembly.
Il visualizzatore della finestra degli strumenti va in errore quando il debug continua Gestire VisualizerTargetStateNotification.Unavailable ed evitare le chiamate ObjectSource fino a quando il target non sarà nuovamente disponibile.

Passaggi successivi