Creación de visualizadores del depurador de Visual Studio

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.

Los visualizadores del depurador son accesibles desde la información sobre datos que aparece al mantener el puntero sobre una variable o desde las ventanas Automático, Variables locales y Inspección:

Screenshot of debugger visualizers in the watch window.

Introducción

Siga la sección Crear el proyecto de extensión en la sección Introducción.

A continuación, agregue una clase que extienda DebuggerVisualizerProvider y aplique el VisualStudioContribution atributo a ella:

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

El código anterior define un nuevo visualizador del depurador, que se aplica a objetos de tipo string:

  • La DebuggerVisualizerProviderConfiguration propiedad define el nombre para mostrar del visualizador y el tipo de .NET admitido.
  • CreateVisualizerAsync Visual Studio invoca el método cuando el usuario solicita la presentación del visualizador del depurador para un valor determinado. CreateVisualizerAsync usa el VisualizerTarget objeto para recuperar el valor que se va a visualizar y pasarlo a un control de usuario remoto personalizado (consulte la documentación de la interfaz de usuario remota). A continuación, se devuelve el control de usuario remoto y se mostrará en una ventana emergente en Visual Studio.

Destino de varios tipos

La propiedad de configuración permite al visualizador tener como destino varios tipos cuando sea conveniente. Un ejemplo perfecto de esto es el visualizador dataset que admite la visualización de DataSetobjetos , DataTable, DataViewy DataViewManager . Esta funcionalidad facilita el desarrollo de extensiones, ya que los tipos similares pueden compartir la misma interfaz de usuario, los modelos de vista y el origen de objetos del visualizador.

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

Origen del objeto del visualizador

El origen del objeto del visualizador es una clase .NET que el depurador carga en el proceso que se está depurando. El visualizador del depurador puede recuperar datos del origen del objeto visualizador mediante métodos expuestos por VisualizerTarget.ObjectSource.

El origen de objeto del visualizador predeterminado permite a los visualizadores del depurador recuperar el valor del objeto que se va a visualizar llamando al RequestDataAsync<T>(JsonSerializer?, CancellationToken) método . El origen de objeto del visualizador predeterminado usa Newtonsoft.Json para serializar el valor y las bibliotecas de extensibilidad de VisualStudio.Extensibility también usan Newtonsoft.Json para la deserialización. Como alternativa, puede usar RequestDataAsync(CancellationToken) para recuperar el valor serializado como .JToken

Si desea visualizar un tipo de .NET compatible de forma nativa con Newtonsoft.Json o desea visualizar su propio tipo y puede hacerlo serializable, las instrucciones anteriores son suficientes para crear un visualizador de depurador simple. Lea si desea admitir tipos más complejos o usar características más avanzadas.

Uso de un origen de objeto de visualizador personalizado

Si Newtonsoft.Json no puede serializar automáticamente el tipo que se va a visualizar, puede crear un origen de objeto de visualizador personalizado para controlar la serialización.

  • Cree un nuevo proyecto de biblioteca de clases de .NET destinado a netstandard2.0. Puede tener como destino una versión más específica de .NET Framework o .NET (por ejemplo, net472 o net6.0) si es necesario para serializar el objeto que se va a visualizar.
  • Agregue una referencia de paquete a la DebuggerVisualizers versión 17.6 o posterior.
  • Agregue una clase que extienda VisualizerObjectSource e invalide GetData la escritura del valor serializado de target en la outgoingData secuencia.
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
        ...
    }
}

Uso de la serialización personalizada

Puede usar el VisualizerObjectSource.SerializeAsJson método para serializar un objeto mediante Newtonsoft.Json a un Stream sin agregar una referencia a Newtonsoft.Json a la biblioteca. La invocación SerializeAsJson cargará, a través de la reflexión, una versión del ensamblado Newtonsoft.Json en el proceso que se está depurando.

Si necesita hacer referencia a Newtonsoft.Json, debe usar la misma versión a la que hace referencia el Microsoft.VisualStudio.Extensibility.Sdk paquete, pero es preferible usar DataContract atributos y DataMember para admitir la serialización de objetos en lugar de confiar en tipos Newtonsoft.Json.

Como alternativa, puede implementar su propia serialización personalizada (como la serialización binaria) escribiendo directamente en outgoingData.

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

Modifique el archivo de extensión .csproj que agrega un ProjectReference elemento al proyecto de biblioteca de origen de objetos del visualizador, lo que garantiza que la biblioteca de origen de objetos del visualizador se compila antes de empaquetar la extensión.

Agregue también un Content elemento que incluya el archivo DLL de la biblioteca de origen de objetos del visualizador en la netstandard2.0 subcarpeta 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>

Como alternativa, puede usar las net4.6.2 subcarpetas o netcoreapp si creó la biblioteca de origen de objetos del visualizador destinada a .NET Framework o .NET. Incluso puede incluir las tres subcarpetas con distintas versiones de la biblioteca de origen de objetos del visualizador, pero es mejor tener como destino netstandard2.0 solo.

Debe intentar minimizar el número de dependencias del archivo DLL de la biblioteca de origen de objetos del visualizador. Si la biblioteca de origen de objetos del visualizador tiene dependencias distintas de Microsoft.VisualStudio.DebuggerVisualizers y bibliotecas que ya están garantizadas de cargarse en el proceso que se está depurando, asegúrese de incluir también esos archivos DLL en la misma subcarpeta que la DLL de la biblioteca de origen del objeto visualizador.

Actualización del proveedor del visualizador del depurador para usar el origen del objeto del visualizador personalizado

A continuación, puede actualizar la DebuggerVisualizerProvider configuración para hacer referencia al origen del objeto del visualizador personalizado:

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

Trabajar con objetos grandes y complejos

Si la recuperación de datos del origen del objeto visualizador no se puede realizar con una sola llamada sin parámetros a RequestDataAsync, puede realizar un intercambio de mensajes más complejo con el origen del objeto visualizador invocando RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) varias veces y enviando mensajes diferentes al origen del objeto visualizador. La infraestructura de extensibilidad de VisualStudio.Extensibility serializa tanto el mensaje como la respuesta mediante Newtonsoft.Json. Otras invalidaciones de RequestDataAsync permiten usar JToken objetos o implementar la serialización y deserialización personalizadas.

Puede implementar cualquier protocolo personalizado mediante mensajes diferentes para recuperar información del origen del objeto visualizador. El caso de uso más común para esta característica es dividir la recuperación de un objeto potencialmente grande en varias llamadas para evitar RequestDataAsync el tiempo de espera.

Este es un ejemplo de cómo puede recuperar el contenido de una colección potencialmente grande un elemento cada vez:

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

El código anterior usa un índice simple como mensaje para las RequestDataAsync llamadas. El código fuente del objeto visualizador correspondiente invalidaría el TransferData método (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);
        }
    }

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

El origen del objeto del visualizador anterior aprovecha el VisualizerObjectSource.DeserializeFromJson método para deserializar el mensaje enviado por el proveedor del visualizador desde incomingData.

Al implementar un proveedor de visualizador de depurador que realiza una interacción compleja de mensajes con el origen del objeto del visualizador, normalmente es mejor pasar al VisualizerTarget visualizador RemoteUserControl para que el intercambio de mensajes pueda producirse de forma asincrónica mientras se carga el control. Pasar VisualizerTarget también permite enviar mensajes al origen del objeto visualizador para recuperar datos en función de las interacciones del usuario con la interfaz de usuario del visualizador.

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

Abrir visualizadores como Ventanas de herramientas

De forma predeterminada, todas las extensiones del visualizador del depurador se abren como ventanas de diálogo modales en primer plano de Visual Studio. Por lo tanto, si el usuario quiere seguir interactuando con el IDE, el visualizador deberá cerrarse. Sin embargo, si la Style propiedad se establece ToolWindow en en la DebuggerVisualizerProviderConfiguration propiedad , el visualizador se abrirá como una ventana de herramientas no modal que puede permanecer abierta durante el resto de la sesión de depuración. Si no se declara ningún estilo, se usará el valor ModalDialog predeterminado.

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

Cada vez que un visualizador opta por abrirse como ToolWindow, deberá suscribirse al evento StateChanged de VisualizerTarget. Cuando se abre un visualizador como una ventana de herramientas, no impedirá que el usuario despase la sesión de depuración. Por lo tanto, el depurador desencadenará el evento mencionado anteriormente siempre que cambie el estado del destino de depuración. Los autores de extensiones del visualizador deben prestar especial atención a estas notificaciones, ya que el destino del visualizador solo está disponible cuando la sesión de depuración está activa y el destino de depuración está en pausa. Cuando el destino del visualizador no está disponible, se producirá un error en las llamadas a ObjectSource métodos 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 notificación se recibirá después RemoteUserControl de que se haya creado y justo antes de que se haga visible en la ventana de herramientas del visualizador recién creada. Mientras el visualizador permanezca abierto, los demás VisualizerTargetStateNotification valores se pueden recibir cada vez que el destino de depuración cambia su estado. La ValueUpdated notificación se usa para indicar que la última expresión abierta por el visualizador se volvió a evaluar correctamente donde el depurador se detuvo y que la interfaz de usuario debe actualizarla. Por otro lado, siempre que se reanude el destino de depuración o no se pueda volver a evaluar la expresión después de detenerla, se recibirá la Unavailable notificación.

Actualización del valor del objeto visualizados

Si VisualizerTarget.IsTargetReplaceable es true, el visualizador del depurador puede usar el ReplaceTargetObjectAsync método para actualizar el valor del objeto visualizado en el proceso que se está depurando.

El origen del objeto del visualizador debe invalidar el CreateReplacementObject método :

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

Pruebe el RegexMatchDebugVisualizer ejemplo para ver estas técnicas en acción.