Visualiseurs du débogueur

Les visualiseurs de débogueur sont une fonctionnalité Visual Studio qui fournit une visualisation personnalisée pour les variables ou les objets d’un type .NET spécifique pendant une session de débogage. Les visualiseurs de débogueur sont accessibles à partir du DataTip qui apparaît lors du survol d’une variable, ou à partir des fenêtres Autos, Locals et Watch.

Note

Cet article utilise le Kit de développement logiciel (SDK) VisualStudio.Extensibility . Pour obtenir de l’aide sur le choix entre VisualStudio.Extensibility et le VSSDK hérité, consultez Choisir le modèle d’extensibilité approprié.

Capture d’écran montrant les visualiseurs du débogueur dans la fenêtre Espion.

Exemple complet

L’implémentation complète se trouve dans l’exemple :

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

Consultez l’exemple de travail complet : RegexMatchDebugVisualizer ↗

Concepts connexes :interface utilisateur distante, contributions et configurations, et créer votre première extension

Comprenez l’exemple

Get started

Pour commencer, créez un projet d’extension VisualStudio.Extensibility en suivant Créer votre première extension.

Chaque extension commence par un point d’entrée Extension qui définit les ExtensionConfiguration métadonnées :

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

Créer le fournisseur de visualisation du débogueur

Ajoutez une classe qui étend DebuggerVisualizerProvider ↗ et appliquez l’attribut VisualStudioContribution ↗ à celui-ci :

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

Ce code définit un nouveau visualiseur de débogueur pour les valeurs string :

  • DebuggerVisualizerProviderConfiguration définit le nom d’affichage du visualiseur et le type .NET pris en charge.
  • CreateVisualizerAsync est appelé lorsque l’utilisateur ouvre le visualiseur. Il utilise VisualizerTarget ↗ pour récupérer les données et renvoie un contrôle Remote UI que Visual Studio affiche dans une fenêtre contextuelle.

Le même objet de configuration est l’endroit où vous déclarez des détails tels que les types cibles, les noms d’affichage du visualiseur, VisualizerObjectSourceTypeet si le visualiseur s’ouvre en tant que boîte de dialogue modale ou en tant que fenêtre d’outil non modale.

Cibler plusieurs types

La propriété de configuration peut cibler plusieurs types lorsqu’il est judicieux de partager la même interface utilisateur, les modèles d’affichage et la même source d’objet visualiseur. Un exemple courant est un visualiseur tabulaire qui prend en charge DataSet, DataTable, DataViewet 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)));

Travailler avec la source de l’objet visualiseur

La source de l'objet visualiseur est une classe .NET que le débogueur charge dans le processus en cours de débogage. Votre visualiseur de débogueur récupère des données à partir de celle-ci via VisualizerTarget.ObjectSource ↗ .

La source d’objet du visualiseur par défaut prend en charge :

L’implémentation par défaut utilise Newtonsoft.Json ↗ pour la sérialisation. Si le type cible est déjà sérialisable ou si vous pouvez rendre votre propre type sérialisable, ce flux par défaut est souvent suffisant.

Utiliser une source d’objet visualiseur personnalisée

Si le type à visualiser ne peut pas être sérialisé automatiquement par Newtonsoft.Json, créez une source d’objet visualiseur personnalisée.

  1. Créez une bibliothèque de classes ciblant netstandard2.0. Si nécessaire, vous pouvez cibler un framework plus spécifique tel que net472 ou net6.0.
  2. Ajoutez une référence de package à Microsoft.VisualStudio.DebuggerVisualizers ↗ version 17.6 ou ultérieure.
  3. Créez une classe qui étend VisualizerObjectSource ↗ et redéfinit 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();
    }
}

Si vous suivez l'exemple Regex, RegexMatchObjectSource convertit Match en une structure RegexMatch sérialisable avant que le fournisseur n'en fasse la demande.

Utiliser la sérialisation personnalisée

Utilisez VisualizerObjectSource.SerializeAsJson pour sérialiser des objets avec Newtonsoft.Json sans ajouter de dépendance directe à Newtonsoft.Json dans la bibliothèque de sources d'objets. La méthode charge l’assembly Newtonsoft.Json dans le processus débogué via la réflexion.

Si vous décidez de référencer Newtonsoft.Json directement, utilisez la même version référencée par Microsoft.VisualStudio.Extensibility.Sdk. Dans la plupart des cas, il est préférable d’utiliser les attributs DataMember et DataContract pour les types partagés au lieu de s’appuyer sur des types spécifiques à Newtonsoft.Json.

Vous pouvez également implémenter votre propre format de sérialisation et écrire directement vers outgoingData.

Ajouter la DLL source de l’objet visualiseur à l’extension

Ajoutez une ProjectReference afin que le projet de sources d'objets soit généré avant l'empaquetage de l'extension, et ajoutez un élément Content afin que le fichier DLL soit copié dans le dossier de sortie de l'extension.

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

Vous pouvez également utiliser net462 ou netcoreapp sous-dossiers si la bibliothèque source d’objets cible ces frameworks, mais netstandard2.0 est préférable si possible.

Réduisez autant que possible le graphe de dépendances de la bibliothèque de sources d'objets. S’il dépend d’autres DLL que le programme débogué ne charge pas déjà, incluez ces DLL dans le même sous-dossier d’extension.

Configurer le fournisseur pour utiliser la source d’objet personnalisée

Une fois la DLL source d’objet empaquetée, mettez à jour DebuggerVisualizerProviderConfiguration pour la pointer vers celle-ci :

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

Si vous évitez intentionnellement une référence d’assembly de l’extension au projet source d’objet, configurez VisualizerObjectSourceType avec le nom qualifié d’assembly au lieu de typeof(...). L’exemple Regex utilise ce modèle.

Travailler avec des objets grands et complexes

Si la récupération de toutes les données en un seul appel à RequestDataAsync sans paramètre n'est pas envisageable, utilisez RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) ↗ pour échanger des messages avec la source d'objets.

Ce modèle est utile pour les grandes collections, car chaque appel à RequestDataAsync a un délai d’expiration de cinq secondes. Une approche courante consiste à demander des entrées une par une :

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

Du côté de la source d'objets, substituez TransferData au lieu de 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 lit le message que le fournisseur a envoyé via incomingData.

Lorsqu'un visualiseur effectue des échanges de messages plus complexes, il est souvent préférable de transmettre VisualizerTarget ↗ au contrôle utilisateur distant afin qu'il puisse demander des données de manière asynchrone pendant son chargement ou ultérieurement, en réponse à une interaction de l'utilisateur :

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

Ouvrir les visualiseurs sous forme de fenêtres Outils

Par défaut, les visualiseurs de débogueur s’ouvrent en tant que boîtes de dialogue modales au premier plan de Visual Studio. Si l’utilisateur souhaite continuer à interagir avec l’IDE, il doit d’abord fermer le visualiseur modal.

Si vous définissez Style = VisualizerStyle.ToolWindow, le visualiseur s’ouvre en tant que fenêtre d’outil non modale et peut rester ouvert pour le reste de la session de débogage. Si vous ne définissez pas Style, la valeur par défaut est ModalDialog.

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

Lorsqu’un visualiseur s’ouvre en tant que fenêtre d’outil, il doit s’abonner à l’événement VisualizerTarget.Changed . Les fenêtres d’outils ne empêchent pas l’utilisateur de reprendre l’exécution. Par conséquent, le débogueur déclenche des notifications chaque fois que la cible de débogage change d’état. Lorsque la cible n’est pas disponible, les appels à ObjectSource échouent avec 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 est déclenché après la création de RemoteUserControl et juste avant que Visual Studio affiche la fenêtre Outils. ValueUpdated indique que l’expression a été correctement réévaluée à l’état d’arrêt actuel et que l’interface utilisateur doit s’actualiser. Unavailable indique que l’exécution a repris son cours ou que l’expression n’a pas pu être réévaluée après l’arrêt de l’exécution.

Mettre à jour la valeur de l’objet visualisée

Si VisualizerTarget.IsTargetReplaceable ↗ c’est truele cas, le visualiseur peut mettre à jour la valeur du processus débogué en appelant ReplaceTargetObjectAsync ↗.

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

La source de l’objet visualiseur doit redéfinir CreateReplacementObject:

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

Tips

✅ DO

  • Placez les métadonnées d’extension dans ExtensionConfiguration et les paramètres de contribution dans DebuggerVisualizerProviderConfiguration.
  • Utilisez netstandard2.0 pour le projet de sources d'objets, sauf si vous avez une raison particulière de cibler un autre framework.
  • Utiliser DataContract et DataMember sur les types de données partagés.
  • Incluez le fichier DLL de la source d'objets et toutes les dépendances requises dans le package de l'extension.
  • Utilisez des appels paginés RequestDataAsync pour des objets volumineux ou coûteux.
  • Gérer VisualizerTarget.Changed lors de l’ouverture en tant que fenêtre d’outil.

❌ NE PAS

  • Ne référencez pas les packages VisualStudio.Extensibility à partir du projet object-source.
  • Ne partez pas du principe que tous les types peuvent être sérialisés par la source d'objets par défaut.
  • N'oubliez pas de faire en sorte que le visualiseur dans une fenêtre Outils prenne en compte les notifications Unavailable.
  • N'oubliez pas que les appels RequestDataAsync peuvent expirer au bout de cinq secondes.

🔧 Résolution des problèmes

Symptôme Que vérifier
Le visualiseur n’apparaît pas Vérifiez que la classe fournisseur a [VisualStudioContribution] et que DebuggerVisualizerProviderConfiguration cible le type .NET correct.
RequestDataAsync échoue ou expire Déplacez la récupération des données volumineuses vers TransferData et répartissez les données sur plusieurs appels à l'aide de la pagination.
La source d’objet personnalisée est introuvable Vérifiez que la DLL est copiée dans le dossier netstandard2.0 de l’extension et VisualizerObjectSourceType pointe vers le type correct ou le nom qualifié d’assembly.
Le visualiseur dans la fenêtre Outils lève une exception lorsque le débogage reprend Gérez VisualizerTargetStateNotification.Unavailable et évitez les appels à ObjectSource jusqu'à ce que la cible soit de nouveau disponible.

Étapes suivantes