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:
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 elVisualizerTarget
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 DataSet
objetos , DataTable
, DataView
y 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
onet6.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 invalideGetData
la escritura del valor serializado detarget
en laoutgoingData
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;
}
Contenido relacionado
Pruebe el RegexMatchDebugVisualizer
ejemplo para ver estas técnicas en acción.