Creare visualizzatori del debugger di Visual Studio

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 dalla descrizione dati visualizzata quando si passa il puntatore del mouse su una variabile o dalle finestre Auto, Variabili locali e Espressioni di controllo :

Screenshot of debugger visualizers in the watch window.

Attività iniziali

Seguire la sezione Creare il progetto di estensione nella sezione Introduzione.

Aggiungere quindi una classe che DebuggerVisualizerProvider estende e applicare l'attributo VisualStudioContribution :

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

Il codice precedente definisce un nuovo visualizzatore del debugger, che si applica agli oggetti di tipo string:

  • La DebuggerVisualizerProviderConfiguration proprietà definisce il nome visualizzatore e il tipo .NET supportato.
  • Il CreateVisualizerAsync metodo viene richiamato da Visual Studio quando l'utente richiede la visualizzazione del visualizzatore del debugger per un determinato valore. CreateVisualizerAsync usa l'oggetto VisualizerTarget per recuperare il valore da visualizzare e passarlo a un controllo utente remoto personalizzato (fare riferimento alla documentazione dell'interfaccia utente remota). Il controllo utente remoto viene quindi restituito e verrà visualizzato in una finestra popup in Visual Studio.

Destinazione di più tipi

La proprietà di configurazione consente al visualizzatore di specificare più tipi quando è utile. Un esempio perfetto di questo è il visualizzatore dataset che supporta la visualizzazione di DataSetoggetti , DataViewDataTable, e DataViewManager . Questa funzionalità semplifica lo sviluppo di estensioni perché tipi simili possono condividere la stessa interfaccia utente, visualizzare i modelli e l'origine oggetti del visualizzatore.

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

Origine oggetto visualizzatore

L'origine oggetto del visualizzatore è una classe .NET caricata dal debugger nel processo di cui è in corso il debug. Il visualizzatore del debugger può recuperare i dati dall'origine oggetto del visualizzatore usando metodi esposti da VisualizerTarget.ObjectSource.

L'origine oggetto visualizzatore predefinito consente ai visualizzatori del debugger di recuperare il valore dell'oggetto da visualizzare chiamando il RequestDataAsync<T>(JsonSerializer?, CancellationToken) metodo . L'origine oggetto visualizzatore predefinito usa Newtonsoft.Json per serializzare il valore e le librerie VisualStudio.Extensibility usano anche Newtonsoft.Json per la deserializzazione. In alternativa, è possibile usare RequestDataAsync(CancellationToken) per recuperare il valore serializzato come JToken.

Se si vuole visualizzare un tipo .NET supportato in modo nativo da Newtonsoft.Json o si vuole visualizzare il proprio tipo ed è possibile renderlo serializzabile, le istruzioni precedenti sono sufficienti per creare un semplice visualizzatore del debugger. Leggere se si desidera supportare tipi più complessi o usare funzionalità più avanzate.

Usare un'origine oggetto visualizzatore personalizzata

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

  • Creare un nuovo progetto di libreria di classi .NET destinato a netstandard2.0. È possibile specificare come destinazione una versione più specifica di .NET Framework o .NET (ad esempio, net472 o net6.0) se necessario per serializzare l'oggetto da visualizzare.
  • Aggiungere un riferimento al pacchetto alla DebuggerVisualizers versione 17.6 o successiva.
  • Aggiungere una classe che VisualizerObjectSource estende ed esegue l'override GetData della scrittura del valore serializzato di target nel outgoingData flusso.
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
        ...
    }
}

Usare la serializzazione personalizzata

È possibile usare il VisualizerObjectSource.SerializeAsJson metodo per serializzare un oggetto usando Newtonsoft.Json in un Stream oggetto senza aggiungere un riferimento a Newtonsoft.Json alla libreria. La chiamata SerializeAsJson verrà caricata, tramite reflection, una versione dell'assembly Newtonsoft.Json nel processo di cui è in corso il debug.

Se è necessario fare riferimento a Newtonsoft.Json, è consigliabile usare la stessa versione a cui fa riferimento il Microsoft.VisualStudio.Extensibility.Sdk pacchetto, ma è preferibile usare DataContract gli attributi e DataMember per supportare la serializzazione degli oggetti anziché basarsi sui tipi Newtonsoft.Json.

In alternativa, è possibile implementare la propria serializzazione personalizzata (ad esempio la serializzazione binaria) scrivendo direttamente in outgoingData.

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

Modificare il file di estensione .csproj aggiungendo un ProjectReference oggetto al progetto di libreria dell'origine dell'oggetto del visualizzatore, assicurandosi che la libreria origine oggetti del visualizzatore venga compilata prima che l'estensione venga inserita nel pacchetto.

Aggiungere anche un Content elemento, inclusa la DLL della libreria di origine dell'oggetto del visualizzatore nella netstandard2.0 sottocartella 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>

In alternativa, è possibile usare le net4.6.2 sottocartelle o netcoreapp se è stata compilata la libreria di origine degli oggetti del visualizzatore destinata a .NET Framework o .NET. È anche possibile includere tutte e tre le sottocartelle con versioni diverse della libreria di origine degli oggetti del visualizzatore, ma è preferibile usare solo come destinazione netstandard2.0 .

È consigliabile provare a ridurre al minimo il numero di dipendenze della DLL della libreria di origine degli oggetti del visualizzatore. Se la libreria di origine dell'oggetto del visualizzatore presenta dipendenze diverse da Microsoft.VisualStudio.DebuggerVisualizers e librerie già caricate nel processo di cui è in corso il debug, assicurarsi di includere anche tali file DLL nella stessa sottocartella della DLL della libreria di origine dell'oggetto del visualizzatore.

Aggiornare il provider del visualizzatore del debugger per usare l'origine dell'oggetto visualizzatore personalizzato

È quindi possibile aggiornare la DebuggerVisualizerProvider configurazione per fare riferimento all'origine dell'oggetto visualizzatore personalizzato:

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

Usare oggetti di grandi dimensioni e complessi

Se il recupero dei dati dall'origine oggetto del visualizzatore non può essere eseguito con una singola chiamata senza parametri a RequestDataAsync, è invece possibile eseguire uno scambio di messaggi più complesso con l'origine oggetto visualizzatore richiamando RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) più volte e inviando messaggi diversi all'origine dell'oggetto visualizzatore. Sia il messaggio che la risposta vengono serializzati dall'infrastruttura VisualStudio.Extensibility usando Newtonsoft.Json. Altre sostituzioni di consentono di RequestDataAsync usare JToken oggetti o implementare la serializzazione e la deserializzazione personalizzate.

È possibile implementare qualsiasi protocollo personalizzato usando messaggi diversi per recuperare informazioni dall'origine oggetto del visualizzatore. Il caso d'uso più comune per questa funzionalità causa l'interruzione del recupero di un oggetto potenzialmente di grandi dimensioni in più chiamate per evitare RequestDataAsync il timeout.

Questo è un esempio di come recuperare il contenuto di una raccolta potenzialmente di grandi dimensioni uno 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);
}

Il codice precedente usa un indice semplice come messaggio per le RequestDataAsync chiamate. Il codice sorgente dell'oggetto visualizzatore corrispondente eseguirà l'override del TransferData metodo (anziché di 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
        ...
    }
}

L'origine oggetto visualizzatore precedente sfrutta il VisualizerObjectSource.DeserializeFromJson metodo per deserializzare il messaggio inviato dal provider del visualizzatore da incomingData.

Quando si implementa un provider del visualizzatore del debugger che esegue un'interazione complessa dei messaggi con l'origine oggetto del visualizzatore, in genere è preferibile passare VisualizerTarget al visualizzatore RemoteUserControl in modo che lo scambio di messaggi possa verificarsi in modo asincrono mentre il controllo viene caricato. Il passaggio VisualizerTarget di consente anche di inviare messaggi all'origine oggetto del visualizzatore per recuperare i dati in base alle interazioni dell'utente con l'interfaccia utente del visualizzatore.

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

Apertura di visualizzatori come Finestre degli strumenti

Per impostazione predefinita, tutte le estensioni del visualizzatore del debugger vengono aperte come finestre di dialogo modali in primo piano di Visual Studio. Pertanto, se l'utente vuole continuare a interagire con l'IDE, il visualizzatore dovrà essere chiuso. Tuttavia, se la Style proprietà è impostata su ToolWindow nella DebuggerVisualizerProviderConfiguration proprietà , il visualizzatore verrà aperto come finestra degli strumenti non modale che può rimanere aperta durante il resto della sessione di debug. Se non viene dichiarato alcuno stile, verrà utilizzato il valore ModalDialog predefinito.

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

Ogni volta che un visualizzatore sceglie di essere aperto come ToolWindow, dovrà sottoscrivere l'evento StateChanged dell'oggetto VisualizerTarget. Quando un visualizzatore viene aperto come finestra degli strumenti, non impedisce all'utente di annullare ilpausing della sessione di debug. Pertanto, l'evento menzionato in precedenza verrà generato dal debugger ogni volta che lo stato della destinazione di debug cambia. Gli autori di estensioni del visualizzatore devono prestare particolare attenzione a queste notifiche, perché la destinazione del visualizzatore è disponibile solo quando la sessione di debug è attiva e la destinazione di debug viene sospesa. Quando la destinazione del visualizzatore non è disponibile, le chiamate ai ObjectSource metodi avranno esito negativo 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;
            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");
            }
        }
    }
}

La Available notifica verrà ricevuta dopo la creazione dell'oggetto RemoteUserControl e appena prima che venga resa visibile nella finestra degli strumenti del visualizzatore appena creata. Finché il visualizzatore rimane aperto, gli altri VisualizerTargetStateNotification valori possono essere ricevuti ogni volta che la destinazione di debug modifica lo stato. La ValueUpdated notifica viene usata per indicare che l'ultima espressione aperta dal visualizzatore è stata rivalutata correttamente in cui il debugger è stato arrestato e deve essere aggiornato dall'interfaccia utente. D'altra parte, ogni volta che la destinazione di debug viene ripresa o l'espressione non può essere rivalutata dopo l'arresto, la Unavailable notifica verrà ricevuta.

Aggiornare il valore dell'oggetto visualizzato

Se VisualizerTarget.IsTargetReplaceable è true, il visualizzatore del debugger può usare il ReplaceTargetObjectAsync metodo per aggiornare il valore dell'oggetto visualizzato nel processo di cui è in corso il debug.

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

public override object CreateReplacementObject(object target, Stream incomingData)
{
    // Use DeserializeFromJson to read from incomingData
    // the new value of the object being visualized
    ...
    return newValue;
}

Provare l'esempio RegexMatchDebugVisualizer per vedere queste tecniche in azione.