Partilhar via


Criar visualizadores de depurador do Visual Studio

Os visualizadores de 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 DataTip que aparece ao passar o mouse sobre uma variável ou das janelas Autos, Locais e Observação :

Captura de ecrã dos visualizadores do depurador na janela de observação.

Introdução

Siga a seção Criar o projeto de extensão na seção Introdução.

Em seguida, adicione uma classe que se estenda 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 de depurador, que se aplica a objetos do tipo string:

  • A DebuggerVisualizerProviderConfiguration propriedade define o nome de exibição do visualizador e o tipo .NET suportado.
  • O CreateVisualizerAsync método é invocado pelo Visual Studio quando o usuário solicita a exibição do visualizador do depurador para um determinado valor. CreateVisualizerAsync usa o VisualizerTarget objeto para recuperar o 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 de configuração permite que o visualizador direcione múltiplos tipos quando apropriado. Um exemplo perfeito disso é o DataSet Visualizer que suporta a visualização de DataSet, DataTable, DataView, e DataViewManager objetos. Esse recurso facilita o desenvolvimento de extensão, uma vez que tipos semelhantes podem compartilhar a mesma interface do usuário, modelos de exibição e fonte 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 do visualizador é uma classe .NET que é carregada pelo depurador no processo que está sendo depurado. O visualizador do depurador pode recuperar dados do objeto de origem do visualizador usando métodos expostos pelo VisualizerTarget.ObjectSource.

O objeto fonte do visualizador padrão permite que os visualizadores do depurador recuperem o valor do objeto a ser visualizado chamando o método RequestDataAsync<T>(JsonSerializer?, CancellationToken). A fonte de objeto do 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, pode usar RequestDataAsync(CancellationToken) para recuperar o valor serializado como JToken.

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 próprio

Se o tipo a ser visualizado não puder ser serializado automaticamente por Newtonsoft.Json, você poderá criar uma fonte de objeto de visualizador personalizada para lidar com a serialização.

  • Crie um novo projeto de biblioteca de classes .NET que tenha como alvo netstandard2.0. 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 que estenda VisualizerObjectSource e substitua GetData para gravar o valor serializado de target no fluxo outgoingData.
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 Microsoft.VisualStudio.Extensibility.Sdk pelo pacote, mas é preferível usar DataContract e DataMember atributos para dar suporte à serialização de objetos em vez de depender de tipos Newtonsoft.Json.

Como alternativa, você pode implementar sua própria serialização personalizada (como serialização binária) gravando diretamente no outgoingData.

Adicione a DLL de origem do objeto do visualizador à extensão

Modifique o arquivo de extensão .csproj adicionando um ProjectReference ao projeto de biblioteca de código-fonte de objeto do visualizador, o que garante que a biblioteca de código-fonte de objeto do visualizador seja criada antes que a extensão seja empacotada.

Adicione também um Content item incluindo a DLL da biblioteca de origem do objeto do 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, pode-se usar as subpastas net4.6.2 ou netcoreapp se criou a biblioteca de origem do objeto do visualizador direcionada ao .NET Framework ou .NET. Você pode até incluir todas as três subpastas com versões diferentes da biblioteca de origem de objetos do visualizador, mas é melhor que o alvo seja apenas netstandard2.0.

Você deve tentar minimizar o número de dependências da DLL da biblioteca de origem de objetos do visualizador. Se a biblioteca de origem de objetos do visualizador tiver dependências diferentes de Microsoft.VisualStudio.DebuggerVisualizers e bibliotecas que já estão garantidas para 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 de objetos do visualizador.

Atualize o fornecedor do visualizador do depurador para utilizar a origem do objeto 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 de objeto do visualizador não puder ser feita com uma única chamada sem parâmetros para RequestDataAsync, você poderá, em vez disso, executar uma troca de mensagens mais complexa com a fonte de objeto do visualizador invocando RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) várias vezes e enviando mensagens diferentes para 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 utilizar objetos JToken ou implementar serialização e desserialização personalizadas.

Você pode implementar qualquer protocolo personalizado usando mensagens diferentes para recuperar informações da fonte de objeto do visualizador. O caso de uso mais comum para esse recurso é dividir 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 de 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);
}

O código acima usa um índice simples como mensagem para as RequestDataAsync chamadas. O código-fonte do objeto do 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 fonte de objeto do visualizador acima utiliza o método VisualizerObjectSource.DeserializeFromJson para desserializar a mensagem enviada pelo fornecedor do visualizador de incomingData.

Ao implementar um provedor de visualizador de depurador que executa interação de mensagem complexa com a fonte de objeto do visualizador, geralmente é melhor passar o VisualizerTarget para o RemoteUserControl visualizador, para que a troca de mensagens possa acontecer de forma assíncrona enquanto o controlo é carregado. Passar o VisualizerTarget também permite enviar 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 modal no primeiro plano do Visual Studio. Assim, 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 ToolWindow na DebuggerVisualizerProviderConfiguration propriedade, o visualizador será aberto como 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á inscrever-se no evento StateChanged do VisualizerTarget. Quando um visualizador é aberto como uma janela de ferramenta, ele não impedirá o usuário de interromper a sessão de depuração. Assim, o evento mencionado será acionado pelo depurador sempre que o estado do alvo de depuração mudar. Os autores da extensão do visualizador devem prestar especial atenção a estas notificações, pois o alvo do visualizador só está disponível quando a sessão de depuração está ativa e o alvo 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.

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 RemoteUserControl ser criada e mesmo antes de ser tornada visível na janela da nova ferramenta de visualização. Enquanto o visualizador permanecer aberto, os outros VisualizerTargetStateNotification valores podem ser recebidos sempre que o alvo de depuração alterar o seu estado. A ValueUpdated notificação é usada para indicar que a última expressão aberta pelo visualizador foi reevaluada com sucesso no ponto em que o depurador parou e deve ser atualizada pela IU. Por outro lado, sempre que o alvo de depuração for retomado ou a expressão não puder ser reavaliada após a interrupção, a Unavailable notificação será recebida.

Atualizar o valor do objeto visualizado

Se VisualizerTarget.IsTargetReplaceable for verdadeiro, o visualizador do depurador pode usar o método ReplaceTargetObjectAsync para atualizar o valor do objeto visualizado no processo em depuração.

A origem do objeto do 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.