Créer des visualiseurs de débogueur Visual Studio

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 de l’info-bulle qui apparaît lors du pointage sur une variable ou à partir des fenêtres Autos, Locals et Watch :

Screenshot of debugger visualizers in the watch window.

Bien démarrer

Suivez la section Créer le projet d’extension dans la section Prise en main.

Ensuite, ajoutez une classe qui étend DebuggerVisualizerProvider et appliquez l’attribut VisualStudioContribution à celui-ci :

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

Le code précédent définit un nouveau visualiseur de débogueur, qui s’applique aux objets de type string:

  • La DebuggerVisualizerProviderConfiguration propriété définit le nom complet du visualiseur et le type .NET pris en charge.
  • La CreateVisualizerAsync méthode est appelée par Visual Studio lorsque l’utilisateur demande l’affichage du visualiseur du débogueur pour une certaine valeur. CreateVisualizerAsync utilise l’objet VisualizerTarget pour récupérer la valeur à visualiser et la transmet à un contrôle utilisateur distant personnalisé (référencez la documentation de l’interface utilisateur distante). Le contrôle utilisateur distant est ensuite retourné et s’affiche dans une fenêtre contextuelle dans Visual Studio.

Ciblage de plusieurs types

La propriété de configuration permet au visualiseur de cibler plusieurs types lorsqu’il est pratique. Voici un exemple parfait du visualiseur DataSet qui prend en charge la visualisation des objets, et DataTableDataViewDataViewManager des DataSetobjets. Cette fonctionnalité facilite le développement d’extensions, car des types similaires peuvent partager la même interface utilisateur, les modèles d’affichage et la même source d’objet visualiseur.

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

Source de l’objet visualiseur

La source de l’objet visualiseur est une classe .NET chargée par le débogueur dans le processus en cours de débogage. Le visualiseur du débogueur peut récupérer des données à partir de la source de l’objet visualiseur à l’aide de méthodes exposées par VisualizerTarget.ObjectSource.

La source d’objet visualiseur par défaut permet aux visualiseurs de débogueur de récupérer la valeur de l’objet à visualiser en appelant la RequestDataAsync<T>(JsonSerializer?, CancellationToken) méthode. La source d’objet visualiseur par défaut utilise Newtonsoft.Json pour sérialiser la valeur, et les bibliothèques VisualStudio.Extensibility utilisent également Newtonsoft.Json pour la désérialisation. Vous pouvez également utiliser RequestDataAsync(CancellationToken) pour récupérer la valeur sérialisée en tant que JToken.

Si vous souhaitez visualiser un type .NET pris en charge en mode natif par Newtonsoft.Json ou que vous souhaitez visualiser votre propre type et vous pouvez le rendre sérialisable, les instructions précédentes sont suffisantes pour créer un visualiseur de débogueur simple. Lisez ce qui suit si vous souhaitez prendre en charge des types plus complexes ou utiliser des fonctionnalités plus avancées.

Utiliser une source d’objet visualiseur personnalisée

Si le type à visualiser ne peut pas être sérialisé automatiquement par Newtonsoft.Json, vous pouvez créer une source d’objet visualiseur personnalisée pour gérer la sérialisation.

  • Créez un projet de bibliothèque de classes .NET ciblant netstandard2.0. Vous pouvez cibler une version plus spécifique de .NET Framework ou .NET (par exemple, net472 ou net6.0) si nécessaire pour sérialiser l’objet à visualiser.
  • Ajoutez une référence de package à la DebuggerVisualizers version 17.6 ou ultérieure.
  • Ajoutez une classe qui étend VisualizerObjectSource et remplace GetData l’écriture de la valeur sérialisée du targetoutgoingData flux.
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
        ...
    }
}

Utiliser la sérialisation personnalisée

Vous pouvez utiliser la VisualizerObjectSource.SerializeAsJson méthode pour sérialiser un objet à l’aide de Newtonsoft.Json vers un Stream sans ajouter de référence à Newtonsoft.Json à votre bibliothèque. L’appel SerializeAsJson se charge, via la réflexion, une version de l’assembly Newtonsoft.Json dans le processus en cours de débogage.

Si vous devez référencer Newtonsoft.Json, vous devez utiliser la même version que celle référencée par le Microsoft.VisualStudio.Extensibility.Sdk package, mais il est préférable d’utiliser et DataMember d’attribuer DataContract des attributs pour prendre en charge la sérialisation d’objets au lieu de compter sur les types Newtonsoft.Json.

Vous pouvez également implémenter votre propre sérialisation personnalisée (par exemple, la sérialisation binaire) en écrivant directement dans outgoingData.

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

Modifiez le fichier d’extension .csproj en ajoutant un ProjectReference projet de bibliothèque de source d’objet visualiseur, ce qui garantit que la bibliothèque de source d’objet visualiseur est générée avant l’empaquetage de l’extension.

Ajoutez également un Content élément incluant la DLL de bibliothèque de source d’objet du visualiseur dans le netstandard2.0 sous-dossier 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 les sous-dossiers ou netcoreapp les net4.6.2 sous-dossiers si vous avez créé la bibliothèque de source d’objet visualiseur ciblant .NET Framework ou .NET. Vous pouvez même inclure les trois sous-dossiers avec différentes versions de la bibliothèque source de l’objet visualiseur, mais il est préférable de cibler netstandard2.0 uniquement.

Vous devez essayer de réduire le nombre de dépendances de la DLL de bibliothèque source d’objet visualiseur. Si votre bibliothèque de source d’objet visualiseur a des dépendances autres que Microsoft.VisualStudio.DebuggerVisualizers et bibliothèques qui sont déjà garanties d’être chargées dans le processus en cours de débogage, veillez à inclure également ces fichiers DLL dans le même sous-dossier que la DLL de bibliothèque de source d’objet visualiseur.

Mettre à jour le fournisseur de visualiseur du débogueur pour utiliser la source d’objet du visualiseur personnalisé

Vous pouvez ensuite mettre à jour votre DebuggerVisualizerProvider configuration pour référencer votre source d’objet visualiseur personnalisée :

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

Utiliser des objets volumineux et complexes

Si la récupération des données à partir de la source d’objet visualiseur ne peut pas être effectuée avec un seul appel sans paramètre, RequestDataAsyncvous pouvez plutôt effectuer un échange de messages plus complexe avec la source de l’objet visualiseur en appelant RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) plusieurs fois et en envoyant différents messages à la source de l’objet visualiseur. Le message et la réponse sont sérialisés par l’infrastructure VisualStudio.Extensibility à l’aide de Newtonsoft.Json. D’autres remplacements vous permettent d’utiliser RequestDataAsyncJToken des objets ou d’implémenter la sérialisation personnalisée et la désérialisation.

Vous pouvez implémenter n’importe quel protocole personnalisé à l’aide de différents messages pour récupérer des informations à partir de la source de l’objet visualiseur. Le cas d’usage le plus courant pour cette fonctionnalité est de briser la récupération d’un objet potentiellement volumineux en plusieurs appels afin d’éviter RequestDataAsync le délai d’attente.

Voici un exemple de la façon dont vous pouvez récupérer le contenu d’une collection potentiellement volumineuse d’un élément à la fois :

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

Le code ci-dessus utilise un index simple comme message pour les RequestDataAsync appels. Le code source de l’objet visualiseur correspondant remplacerait la TransferData méthode (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);
        }
    }

    private static MySerializableType Convert(object target)
    {
        // Add your code here to convert target into a type serializable by Newtonsoft.Json
        ...
    }
}

La source de l’objet visualiseur ci-dessus tire parti de la VisualizerObjectSource.DeserializeFromJson méthode pour désérialiser le message envoyé par le fournisseur de visualiseur à partir de incomingData.

Lors de l’implémentation d’un fournisseur de visualiseur de débogueur qui effectue une interaction de message complexe avec la source de l’objet visualiseur, il est généralement préférable de passer les VisualizerTarget données au visualiseur RemoteUserControl afin que l’échange de messages puisse se produire de manière asynchrone pendant le chargement du contrôle. La transmission des VisualizerTarget messages à la source d’objet du visualiseur vous permet également de récupérer des données en fonction des interactions de l’utilisateur avec l’interface utilisateur du visualiseur.

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

Ouverture de visualiseurs en tant qu’outil Windows

Par défaut, toutes les extensions du visualiseur de débogueur sont ouvertes en tant que fenêtres de dialogue modales au premier plan de Visual Studio. Par conséquent, si l’utilisateur souhaite continuer à interagir avec l’IDE, le visualiseur doit être fermé. Toutefois, si la Style propriété est définie ToolWindow dans la DebuggerVisualizerProviderConfiguration propriété, le visualiseur est ouvert en tant que fenêtre d’outil non modale qui peut rester ouverte pendant le reste de la session de débogage. Si aucun style n’est déclaré, la valeur ModalDialog par défaut est utilisée.

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

Chaque fois qu’un visualiseur choisit d’être ouvert en tant que ToolWindow, il devra s’abonner à l’événement StateChanged de l’objet VisualizerTarget. Lorsqu’un visualiseur est ouvert en tant que fenêtre d’outil, il ne empêche pas l’utilisateur de supprimer la session de débogage. Par conséquent, l’événement mentionné ci-dessus sera déclenché par le débogueur chaque fois que l’état de la cible de débogage change. Les auteurs d’extensions du visualiseur doivent prêter une attention particulière à ces notifications, car la cible du visualiseur est disponible uniquement lorsque la session de débogage est active et que la cible de débogage est suspendue. Lorsque la cible du visualiseur n’est pas disponible, les appels aux ObjectSource méthodes échouent avec un 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 notification est reçue après la création de la RemoteUserControl notification et juste avant qu’elle ne soit rendue visible dans la fenêtre de l’outil de visualiseur nouvellement créée. Tant que le visualiseur reste ouvert, les autres VisualizerTargetStateNotification valeurs peuvent être reçues chaque fois que la cible de débogage change son état. La ValueUpdated notification est utilisée pour indiquer que la dernière expression ouverte par le visualiseur a été correctement réévaluée où le débogueur s’est arrêté et doit être actualisé par l’interface utilisateur. En revanche, chaque fois que la cible de débogage est reprise ou que l’expression ne peut pas être réévaluée après l’arrêt, la Unavailable notification est reçue.

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

Si VisualizerTarget.IsTargetReplaceable la valeur est true, le visualiseur du débogueur peut utiliser la ReplaceTargetObjectAsync méthode pour mettre à jour la valeur de l’objet visualisé dans le processus en cours de débogage.

La source de l’objet visualiseur doit remplacer la CreateReplacementObject méthode :

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

Essayez l’exemple RegexMatchDebugVisualizer pour voir ces techniques en action.