Criar visualizadores de depurador do Visual Studio
Os visualizadores do depurador são um recurso do Visual Studio que fornece uma visualização personalizada para variáveis ou objetos de um tipo .NET específico durante uma sessão de depuração.
Os visualizadores do depurador podem ser acessados a partir da Dica de Dados que aparece ao passar o mouse sobre uma variável ou das janelas Autos, Locais e Inspeção :
Introdução
Siga a seção Criar o projeto de extensão na seção Introdução.
Em seguida, adicione uma classe estendendo DebuggerVisualizerProvider
e aplique o VisualStudioContribution
atributo a ela:
/// <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);
}
}
O código anterior define um novo visualizador do depurador, que se aplica a objetos do tipo string
:
- A
DebuggerVisualizerProviderConfiguration
propriedade define o nome de exibição do visualizador e o tipo .NET com suporte. - O
CreateVisualizerAsync
método é chamado pelo Visual Studio quando o usuário solicita a exibição do visualizador do depurador para um determinado valor.CreateVisualizerAsync
usa o objeto para recuperar oVisualizerTarget
valor a ser visualizado e o passa para um controle de usuário remoto personalizado (consulte a documentação da interface do usuário remota). O controle de usuário remoto é retornado e será mostrado em uma janela pop-up no Visual Studio.
Segmentação de vários tipos
A propriedade configuration permite que o visualizador direcione vários tipos quando conveniente. Um exemplo perfeito disso é o DataSet Visualizer , que oferece suporte à visualização de DataSet
objetos , DataTable
, DataView
e DataViewManager
. Esse recurso facilita o desenvolvimento de extensões, já que tipos semelhantes podem compartilhar a mesma interface do usuário, modelos de exibição e origem de objeto do 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)
{
...
}
A origem do objeto do visualizador
A origem do objeto visualizador é uma classe .NET que é carregada pelo depurador no processo que está sendo depurado. O visualizador do depurador pode recuperar dados da fonte do objeto do visualizador usando métodos expostos pelo VisualizerTarget.ObjectSource
.
A origem do objeto do visualizador padrão permite que os visualizadores do depurador recuperem o valor do objeto a ser visualizado chamando o RequestDataAsync<T>(JsonSerializer?, CancellationToken)
método. A origem do objeto visualizador padrão usa Newtonsoft.Json para serializar o valor, e as bibliotecas VisualStudio.Extensibility também usam Newtonsoft.Json para a desserialização. Como alternativa, você pode usar RequestDataAsync(CancellationToken)
para recuperar o valor serializado como um JToken
arquivo .
Se você deseja visualizar um tipo .NET que é suportado nativamente por Newtonsoft.Json, ou você deseja visualizar seu próprio tipo e você pode torná-lo serializável, as instruções anteriores são suficientes para criar um visualizador de depurador simples. Continue lendo se quiser oferecer suporte a tipos mais complexos ou usar recursos mais avançados.
Usar uma fonte de objeto do visualizador personalizado
Se o tipo a ser visualizado não puder ser serializado automaticamente por Newtonsoft.Json, você poderá criar uma fonte de objeto visualizador personalizada para manipular a serialização.
- Crie uma nova segmentação
netstandard2.0
de projeto de biblioteca de classes .NET. Você pode direcionar uma versão mais específica do .NET Framework ou .NET (por exemplo,net472
ounet6.0
) se necessário para serializar o objeto a ser visualizado. - Adicione uma referência de pacote à
DebuggerVisualizers
versão 17.6 ou mais recente. - Adicione uma classe estendendo
VisualizerObjectSource
e substituaGetData
a gravação do valor serializado detarget
nooutgoingData
fluxo.
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
...
}
}
Usar serialização personalizada
Você pode usar o VisualizerObjectSource.SerializeAsJson
método para serializar um objeto usando Newtonsoft.Json para um Stream
sem adicionar uma referência a Newtonsoft.Json à sua biblioteca. A invocação SerializeAsJson
carregará, via reflexão, uma versão do assembly Newtonsoft.Json no processo que está sendo depurado.
Se você precisar fazer referência a Newtonsoft.Json, deverá usar a mesma versão referenciada pelo Microsoft.VisualStudio.Extensibility.Sdk
pacote, mas é preferível usar DataContract
atributos e DataMember
para oferecer suporte à serialização de objetos em vez de depender dos tipos Newtonsoft.Json.
Como alternativa, você pode implementar sua própria serialização personalizada (como serialização binária) gravando diretamente no outgoingData
.
Adicionar a DLL de origem do objeto visualizador à extensão
Modifique o arquivo de extensão adicionando um ao projeto de ProjectReference
biblioteca de código-fonte do objeto visualizador, o que garante que a biblioteca de código-fonte do objeto visualizador seja criada antes que a extensão .csproj
seja empacotada.
Adicione também um Content
item incluindo a DLL da biblioteca de origem do objeto visualizador na netstandard2.0
subpasta da extensão.
<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, você pode usar as net4.6.2
subpastas ou se tiver criado a biblioteca de origem de objetos do visualizador direcionada ao .NET Framework ou netcoreapp
.NET. Você pode até incluir todas as três subpastas com versões diferentes da biblioteca de origem do objeto visualizador, mas é melhor segmentar netstandard2.0
apenas.
Você deve tentar minimizar o número de dependências da DLL da biblioteca de origem do objeto visualizador. Se a biblioteca de origem do objeto do visualizador tiver dependências diferentes de Microsoft.VisualStudio.DebuggerVisualizers e bibliotecas que já têm garantia de serem carregadas no processo que está sendo depurado, certifique-se de incluir também esses arquivos DLL na mesma subpasta que a DLL da biblioteca de origem do objeto do visualizador.
Atualizar o provedor do visualizador do depurador para usar a origem do objeto do visualizador personalizado
Em seguida, você pode atualizar sua DebuggerVisualizerProvider
configuração para fazer referência à origem do objeto do 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);
}
Trabalhar com objetos grandes e complexos
Se a recuperação de dados da fonte do objeto do visualizador não puder ser feita com uma única chamada sem parâmetros para o , você poderá executar uma troca de mensagens mais complexa com a fonte do objeto do visualizador invocando RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken)
várias vezes e enviando mensagens diferentes para RequestDataAsync
a fonte do objeto do visualizador. A mensagem e a resposta são serializadas pela infraestrutura VisualStudio.Extensibility usando Newtonsoft.Json. Outras substituições de RequestDataAsync
permitem que você use JToken
objetos ou implemente serialização e desserialização personalizadas.
Você pode implementar qualquer protocolo personalizado usando mensagens diferentes para recuperar informações da origem do objeto visualizador. O caso de uso mais comum para esse recurso é quebrar a recuperação de um objeto potencialmente grande em várias chamadas para evitar RequestDataAsync
o tempo limite.
Este é um exemplo de como você pode recuperar o conteúdo de uma coleção potencialmente grande, um item por 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);
}
O código acima usa um índice simples como mensagem para as RequestDataAsync
chamadas. O código-fonte do objeto visualizador correspondente substituiria o TransferData
método (em vez 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
...
}
}
A origem do objeto do visualizador acima aproveita o método para desserializar a mensagem enviada pelo provedor do visualizador do VisualizerObjectSource.DeserializeFromJson
incomingData
.
Ao implementar um provedor de visualizador de depurador que executa interação de mensagem complexa com a origem do objeto visualizador, geralmente é melhor passar o VisualizerTarget
para o visualizador para que a troca de RemoteUserControl
mensagens possa acontecer de forma assíncrona enquanto o controle é carregado. Passar o VisualizerTarget
também permite que você envie mensagens para a fonte de objeto do visualizador para recuperar dados com base nas interações do usuário com a interface do usuário do 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
...
}
...
Abrindo visualizadores como janelas de ferramentas
Por padrão, todas as extensões do visualizador do depurador são abertas como janelas de diálogo modais no primeiro plano do Visual Studio. Portanto, se o usuário quiser continuar a interagir com o IDE, o visualizador precisará ser fechado. No entanto, se a Style
propriedade estiver definida como na DebuggerVisualizerProviderConfiguration
propriedade, o visualizador será aberto como ToolWindow
uma janela de ferramenta não modal que pode permanecer aberta durante o restante da sessão de depuração. Se nenhum estilo for declarado, o valor ModalDialog
padrão será usado.
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);
}
Sempre que um visualizador optar por ser aberto como um ToolWindow
, ele precisará se inscrever no evento StateChanged do VisualizerTarget
. Quando um visualizador é aberto como uma janela de ferramenta, ele não impede o usuário de cancelar a pausa da sessão de depuração. Assim, o evento mencionado acima será acionado pelo depurador sempre que o estado do destino de depuração for alterado. Os autores da extensão do visualizador devem prestar atenção especial a essas notificações, pois o destino do visualizador só está disponível quando a sessão de depuração está ativa e o destino de depuração está pausado. Quando o destino do visualizador não estiver disponível, as chamadas para ObjectSource
métodos falharão com um VisualizerTargetUnavailableException
arquivo .
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");
}
}
}
}
A Available
notificação será recebida após a criação e pouco antes de RemoteUserControl
ficar visível na janela de ferramentas do visualizador recém-criada. Enquanto o visualizador permanecer aberto, os outros VisualizerTargetStateNotification
valores podem ser recebidos sempre que o destino de depuração alterar seu estado. A ValueUpdated
notificação é usada para indicar que a última expressão aberta pelo visualizador foi reavaliada com êxito onde o depurador parou e deve ser atualizada pela interface do usuário. Por outro lado, sempre que o destino de depuração for retomado ou a expressão não puder ser reavaliada após a parada, a Unavailable
notificação será recebida.
Atualizar o valor do objeto visualizado
Se VisualizerTarget.IsTargetReplaceable
for true, o visualizador do depurador poderá usar o ReplaceTargetObjectAsync
método para atualizar o valor do objeto visualizado no processo que está sendo depurado.
A origem do objeto visualizador deve substituir o 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;
}
Conteúdo relacionado
Experimente o RegexMatchDebugVisualizer
exemplo para ver essas técnicas em ação.