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 :

Screenshot of debugger visualizers in the watch window.

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 o VisualizerTarget 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 DataSetobjetos , DataTable, DataViewe 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 JTokenarquivo .

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.0de projeto de biblioteca de classes .NET. Você pode direcionar uma versão mais específica do .NET Framework ou .NET (por exemplo, net472 ou net6.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 substitua GetData a gravação do valor serializado de target no outgoingData 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 RequestDataAsynca 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.DeserializeFromJsonincomingData.

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

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

Experimente o RegexMatchDebugVisualizer exemplo para ver essas técnicas em ação.