Visualizadores del depurador

Los visualizadores del depurador son una característica de Visual Studio que proporciona una visualización personalizada para variables u objetos de un tipo .NET específico durante una sesión de depuración. Se puede acceder a los visualizadores del depurador desde el DataTip que aparece al pasar el puntero sobre una variable, o desde las ventanas Automático, Locales e Inspección.

Note

En este artículo se usa el SDK VisualStudio.Extensibility. Para obtener ayuda para elegir entre VisualStudio.Extensibility y el VSSDK heredado, consulte Elegir el modelo de extensibilidad adecuado.

Captura de pantalla en la que se muestran los visualizadores del depurador en la ventana Inspección.

Ejemplo completo

La implementación completa está en el ejemplo:

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

Consulte el ejemplo de trabajo completo: RegexMatchDebugVisualizer ↗

Conceptos relacionados:Interfaz de usuario remota, Contribuciones y configuraciones y Crea tu primera extensión

Entiende el ejemplo

Get started

Para empezar, cree un proyecto de extensión VisualStudio.Extensibility siguiendo Creación de la primera extensión.

Cada extensión comienza con un Extension punto de entrada que define los metadatos ExtensionConfiguration :

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

Creación del proveedor del visualizador del depurador

Agregue una clase que extienda DebuggerVisualizerProvider ↗ y aplíquele el atributo VisualStudioContribution ↗:

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

Este código define un nuevo visualizador del depurador para valores string:

  • DebuggerVisualizerProviderConfigurationdefine el nombre para mostrar del visualizador y el tipo de .NET admitido.
  • CreateVisualizerAsync se invoca cuando el usuario abre el visualizador. Usa VisualizerTarget para recuperar datos y devuelve un control Remote UI que Visual Studio muestra en una ventana emergente.

El mismo objeto de configuración es donde se declaran detalles como los tipos de destino, los nombres para mostrar del visualizador, VisualizerObjectSourceTypey si el visualizador se abre como un cuadro de diálogo modal o como una ventana de herramientas no modal.

Destinarse a varios tipos

La propiedad de configuración puede aplicarse a varios tipos cuando tenga sentido compartir la misma interfaz de usuario, los modelos de vista y la fuente de objetos del visualizador. Un ejemplo común es un visualizador tabular que admite DataSet, DataTable, DataViewy 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)));

Trabajar con el origen del objeto del visualizador

El origen del objeto del visualizador es una clase de .NET que el depurador carga en el proceso que se está depurando. El visualizador del depurador recupera datos de él a través de VisualizerTarget.ObjectSource ↗.

El origen de objeto del visualizador predeterminado admite:

La implementación predeterminada usa Newtonsoft.Json ↗ para la serialización. Si el tipo de destino ya es serializable o si puede hacer que su propio tipo sea serializable, este flujo predeterminado suele ser suficiente.

Utilizar un origen de objeto de visualizador personalizado

Si el tipo que se va a visualizar no puede serializarse automáticamente mediante Newtonsoft.Json, cree un origen de objeto del visualizador personalizado.

  1. Cree una biblioteca de clases que tenga como destino netstandard2.0. Si es necesario, puede tener como destino un marco más específico, como net472 o net6.0.
  2. Agregue una referencia de paquete a Microsoft. VisualStudio.DebuggerVisualizers ↗, versión 17.6 o posterior.
  3. Cree una clase que extienda VisualizerObjectSource ↗ y sobrescriba 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 sigue el ejemplo Regex, RegexMatchObjectSource convierte Match en una forma serializable RegexMatch antes de que el proveedor lo solicite.

Uso de la serialización personalizada

Utilice VisualizerObjectSource.SerializeAsJson para serializar objetos con Newtonsoft.Json sin agregar una dependencia directa de Newtonsoft.Json a la biblioteca de origen de objetos. El método carga el ensamblado Newtonsoft.Json en el proceso depurado a través de la reflexión.

Si decide hacer referencia directamente a Newtonsoft.Json, use la misma versión a la que hace referencia Microsoft.VisualStudio.Extensibility.Sdk. En la mayoría de los casos, es mejor usar los atributos DataContract y DataMember para tipos compartidos en lugar de confiar en tipos específicos de Newtonsoft.Json.

También puede implementar su propio formato de serialización y escribir directamente en outgoingData.

Adición del archivo DLL de origen del objeto del visualizador a la extensión

Agregue un ProjectReference para que el proyecto del origen del objeto se compile antes de empaquetar la extensión y agregue un elemento Content para que el DLL se copie en la salida de la extensión.

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

También puede usar net462 o netcoreapp subcarpetas si la biblioteca de origen de objetos tiene como destino esos marcos, pero netstandard2.0 se prefiere siempre que sea posible.

Mantenga el gráfico de dependencias de la biblioteca object-source lo más pequeño posible. Si depende de otros archivos DLL que el programa depurado aún no carga, incluya esos archivos DLL en la misma subcarpeta de extensión.

Configurar el proveedor para que use el origen de objeto personalizado

Después de empaquetar la DLL de origen del objeto, actualice DebuggerVisualizerProviderConfiguration para que apunte a ella:

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 evita intencionadamente una referencia de ensamblado de la extensión al proyecto de origen del objeto, configure VisualizerObjectSourceType con el nombre completo del ensamblado en lugar de typeof(...). El ejemplo Regex usa ese patrón.

Trabajar con objetos grandes y complejos

Si no es práctico recuperar todos los datos en una única llamada sin parámetros a RequestDataAsync, use RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) ↗ para intercambiar mensajes con el origen del objeto.

Este patrón es útil para colecciones grandes porque cada llamada a RequestDataAsync tiene un tiempo de espera de cinco segundos. Un enfoque común es solicitar entradas de uno en uno:

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

En el lado del origen del objeto, invalide TransferData en lugar 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 lee el mensaje que el proveedor envió a través de incomingData.

Cuando un visualizador realiza intercambios de mensajes más complejos, suele ser mejor pasar el VisualizerTarget ↗ al control de usuario remoto para que pueda solicitar datos de forma asincrónica mientras se carga el control o posteriormente en respuesta a la interacción del usuario:

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

Abrir visualizadores como ventanas de herramientas

De forma predeterminada, los visualizadores del depurador se abren como diálogos modales en primer plano de Visual Studio. Si el usuario quiere seguir interactuando con el IDE, primero debe cerrar el visualizador modal.

Si establece Style = VisualizerStyle.ToolWindow, el visualizador se abre como una ventana de herramientas no modal y puede permanecer abierto para el resto de la sesión de depuración. Si no establece Style, el valor predeterminado es ModalDialog.

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

Cuando se abre un visualizador como una ventana de herramientas, debe suscribirse al VisualizerTarget.Changed evento. Las ventanas de herramientas no impiden que el usuario reanude la ejecución, por lo que el depurador genera notificaciones cada vez que cambia el estado del destino de depuración. Cuando el destino no está disponible, las llamadas a ObjectSource dan error 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");
            }
        }
    }
}

Available se genera después de que se cree el RemoteUserControl y justo antes de que Visual Studio muestre la ventana de herramientas. ValueUpdated indica que la expresión se ha vuelto a evaluar correctamente en el estado de interrupción actual y que la interfaz de usuario debe actualizarse. Unavailable indica que la ejecución se reanudó o que la expresión no se pudo volver a evaluar después de que se detuvo la ejecución.

Actualizar el valor del objeto visualizado

Si VisualizerTarget.IsTargetReplaceable ↗ es true, el visualizador puede actualizar el valor del depurado llamando a ReplaceTargetObjectAsync ↗ .

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

El origen del objeto del visualizador debe invalidar CreateReplacementObject:

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

Tips

✅ DO

  • Coloque los metadatos de extensión en ExtensionConfiguration y la configuración de contribución en DebuggerVisualizerProviderConfiguration.
  • Use netstandard2.0 para el proyecto del origen del objeto a menos que tenga una razón específica para dirigirse a otro marco.
  • Use DataContract y DataMember en los tipos de datos compartidos.
  • Empaquete el DLL del origen del objeto y las dependencias necesarias con la extensión.
  • Use las llamadas paginadas a RequestDataAsync para los objetos grandes o costosos.
  • Controle VisualizerTarget.Changed al abrirlo como una ventana de herramientas.

❌ INCORRECTO

  • No haga referencia a los paquetes de VisualStudio.Extensibility desde el proyecto del origen del objeto.
  • No suponga que todos los tipos pueden serializarse mediante el origen del objeto predeterminado.
  • No deje un visualizador de la ventana de herramientas sin tener en cuenta las notificaciones de Unavailable.
  • No olvide que las llamadas a RequestDataAsync pueden agotar el tiempo de espera después de cinco segundos.

🔧 Solución de problemas

Síntoma Elementos que se deben comprobar
El visualizador no aparece Compruebe que la clase de proveedor tiene [VisualStudioContribution] y que DebuggerVisualizerProviderConfiguration tiene como destino el tipo de .NET correcto.
RequestDataAsync falla o se agota el tiempo de espera Mueva la recuperación de datos de gran tamaño a TransferData y pagine los datos en varias llamadas.
No se encuentra el origen del objeto personalizado Confirme que el DLL se copia en la carpeta netstandard2.0 de la extensión y que VisualizerObjectSourceType apunta al tipo o nombre calificado por el ensamblado correctos.
El visualizador de la ventana de herramientas inicia una excepción cuando continúa la depuración Controle VisualizerTargetStateNotification.Unavailable y evite las llamadas a ObjectSource hasta que el destino vuelva a estar disponible.

Pasos siguientes