Visual Studio 디버거 시각화 도우미 만들기

디버거 시각화 도우미는 디버그 세션 중에 특정 .NET 형식의 변수 또는 개체에 대한 사용자 지정 시각화를 제공하는 Visual Studio 기능입니다.

디버거 시각화기는 변수 위를 맴돌 때 나타나는 DataTip 또는 Autos, Local, Watch 창에서 액세스할 수 있습니다:

Screenshot of debugger visualizers in the watch window.

시작하기

시작 섹션의 확장 프로젝트 만들기 섹션을 따릅니다.

그런 다음 확장 클래스를 DebuggerVisualizerProvider 추가하고 특성을 적용 VisualStudioContribution 합니다.

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

이전 코드는 형식 string의 개체에 적용되는 새 디버거 시각화 도우미를 정의합니다.

  • 이 속성은 DebuggerVisualizerProviderConfiguration 시각화 도우미 표시 이름과 지원되는 .NET 형식을 정의합니다.
  • CreateVisualizerAsync 이 메서드는 사용자가 특정 값에 대한 디버거 시각화 도우미의 표시를 요청할 때 Visual Studio에서 호출됩니다. CreateVisualizerAsyncVisualizerTarget 개체를 사용하여 시각화할 값을 검색하고 사용자 지정 원격 사용자 정의 컨트롤에 전달합니다(이 원격 UI 설명서 참조). 그런 다음 원격 사용자 컨트롤이 반환되고 Visual Studio의 팝업 창에 표시됩니다.

여러 유형 타겟팅

구성 속성을 사용하면 시각화 도우미가 편리한 경우 여러 형식을 대상으로 지정할 수 있습니다. 이것의 완벽한 예는 DataSet, DataTable, DataViewDataViewManager 개체의 시각화를 지원하는 DataSet Visualizer입니다. 이 기능은 유사한 형식이 동일한 UI, 뷰 모델 및 시각화 도우미 개체 원본을 공유할 수 있으므로 확장 개발을 용이하게 합니다.

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

시각화 개체 원본

시각화 도우미 개체 원본은 디버그 중인 프로세스에서 디버거에 의해 로드되는 .NET 클래스입니다. 디버거 시각화 도우미는 에 의해 VisualizerTarget.ObjectSource노출되는 메서드를 사용하여 시각화 도우미 개체 원본에서 데이터를 검색할 수 있습니다.

기본 시각화 도우미 개체 원본을 사용하면 디버거 시각화 도우미가 메서드를 호출 RequestDataAsync<T>(JsonSerializer?, CancellationToken) 하여 시각화할 개체의 값을 검색할 수 있습니다. 기본 시각화 도우미 개체 원본은 Newtonsoft.Json을 사용하여 값을 직렬화하고 VisualStudio.Extensibility 라이브러리는 역직렬화에 Newtonsoft.Json도 사용합니다. 또는 RequestDataAsync(CancellationToken) 을 사용하여 직렬화된 값을 JToken로 검색할 수 있습니다.

Newtonsoft.Json에서 기본적으로 지원되는 .NET 형식을 시각화하거나 고유한 형식을 시각화하고 직렬화할 수 있도록 하려는 경우 이전 지침은 간단한 디버거 시각화 도우미를 만들기에 충분합니다. 더 복잡한 형식을 지원하거나 고급 기능을 사용하려면 계속 읽어보세요.

사용자 지정 시각화 도우미 개체 원본 사용

Newtonsoft.Json에서 시각화할 형식을 자동으로 직렬화할 수 없는 경우 직렬화를 처리하는 사용자 지정 시각화 도우미 개체 원본을 만들 수 있습니다.

  • netstandard2.0을 대상으로 새 .NET 클래스 라이브러리 프로젝트를 만듭니다. 시각화할 개체를 직렬화하는 데 필요한 경우 더 특정 버전의 .NET Framework 또는 .NET(예: net472 또는 net6.0)을 대상으로 할 수 있습니다.
  • DebuggerVisualizers 버전 17.6 이상에 패키지 참조를 추가합니다.
  • VisualizerObjectSource 확장 클래스를 추가하고 outgoingData 스트림에 target 의 직렬 값을 기록하는 GetData 를 재정의합니다.
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
        ...
    }
}

사용자 지정 직렬화 사용

VisualizerObjectSource.SerializeAsJson 메서드를 사용하여 Newtonsoft.Json에 대한 참조를 라이브러리에 추가하지 않고 Stream 에 Newtonsoft.Json을 사용하여 개체를 직렬화할 수 있습니다. SerializeAsJson 호출은 리플렉션을 통해 Newtonsoft.Json 어셈블리의 버전을 디버그 중인 프로세스에 로드합니다.

Newtonsoft.Json를 참조해야 하는 경우 Microsoft.VisualStudio.Extensibility.Sdk 패키지에서 참조하는 버전과 동일한 버전을 사용해야 하지만, Newtonsoft.Json 유형에 의존하는 대신 객체 직렬화를 지원하기 위해 DataContractDataMember 속성을 사용하는 것이 좋습니다.

또는 직접 쓰는 사용자 지정 serialization(예: 이진 serialization)을 구현할 수 있습니다 outgoingData.

확장에 시각화 도우미 개체 원본 DLL 추가

확장 프로그램 .csproj 파일을 수정하여 비주얼라이저 개체 소스 라이브러리 프로젝트에 ProjectReference 를 추가합니다. 그러면 확장 프로그램이 패키지되기 전에 비주얼라이저 개체 소스 라이브러리가 구축되었는지 확인합니다.

또한 visualizer 객체 소스 라이브러리 DLL을 포함하는 Content 항목을 확장의 netstandard2.0 하위 폴더에 추가합니다.

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

또는 .NET Framework 또는 .NET을 대상으로 visualizer 개체 소스 라이브러리를 구축한 경우 net4.6.2 또는 netcoreapp 하위 폴더를 사용할 수 있습니다. 시각화 도우미 개체 원본 라이브러리의 버전이 서로 다른 세 하위 폴더를 모두 포함할 수도 있지만 대상으로 netstandard2.0 만 지정하는 것이 좋습니다.

시각화 도우미 개체 원본 라이브러리 DLL의 종속성 수를 최소화해야 합니다. 시각화 도우미 개체 원본 라이브러리에 Microsoft.VisualStudio.DebuggerVisualizers 및 디버그 중인 프로세스에서 이미 로드되도록 보장된 라이브러리 이외의 종속성이 있는 경우 해당 DLL 파일도 시각화 도우미 개체 원본 라이브러리 DLL과 동일한 하위 폴더에 포함해야 합니다.

사용자 지정 시각화 도우미 개체 원본을 사용하도록 디버거 시각화 도우미 공급자 업데이트

그런 다음 사용자 지정 시각화 도우미 개체 원본을 참조하도록 구성을 업데이트 DebuggerVisualizerProvider 할 수 있습니다.

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

크고 복잡한 개체 작업

시각화 도우미 개체 원본에서 데이터 검색을 단일 매개 변수 없는 호출 RequestDataAsync로 수행할 수 없는 경우 시각화 도우미 개체 원본을 여러 번 호출 RequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken) 하고 다른 메시지를 시각화 도우미 개체 원본으로 전송하여 보다 복잡한 메시지 교환을 수행할 수 있습니다. 메시지와 응답은 Newtonsoft.Json을 사용하여 VisualStudio.Extensibility 인프라에 의해 직렬화됩니다. 다른 재정의 RequestDataAsync 를 통해 개체를 사용 JToken 하거나 사용자 지정 serialization 및 역직렬화를 구현할 수 있습니다.

다른 메시지를 사용하여 시각화 도우미 개체 원본에서 정보를 검색하는 사용자 지정 프로토콜을 구현할 수 있습니다. 이 기능의 가장 일반적인 사용 사례는 잠재적으로 큰 개체의 검색을 여러 호출로 분리하여 시간 초과를 방지하는 RequestDataAsync 것입니다.

이는 잠재적으로 큰 컬렉션의 콘텐츠를 한 번에 하나의 항목으로 검색하는 방법의 예입니다.

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

위의 코드는 RequestDataAsync 호출에 대한 메시지로 간단한 인덱스 사용 해당 시각화 도우미 개체 소스 코드는 다음 대신 메서드를 TransferData 재정의 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
        ...
    }
}

위의 시각화 도우미 개체 원본은 메서드를 VisualizerObjectSource.DeserializeFromJson 활용하여 시각화 도우미 공급자 incomingData가 보낸 메시지를 역직렬화합니다.

시각화 개체 소스와 복잡한 메시지 상호 작용을 수행하는 디버거 시각화 제공자를 구현할 때, 일반적으로 VisualizerTarget 을 시각화자의 RemoteUserControl 에 전달하여 컨트롤이 로드되는 동안 메시지 교환이 비동기적으로 발생할 수 있도록 하는 것이 좋습니다. 또한 이를 VisualizerTarget 전달하면 시각화 도우미 개체 원본에 메시지를 보내 시각화 도우미의 UI와 사용자의 상호 작용에 따라 데이터를 검색할 수 있습니다.

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

도구 창으로 시각화 도우미 열기

기본적으로 모든 디버거 시각화 도우미 확장은 Visual Studio 전경에서 모달 대화 상자 창으로 열립니다. 따라서 사용자가 IDE와 계속 상호 작용하려는 경우 시각화 도우미를 닫아야 합니다. 그러나 DebuggerVisualizerProviderConfiguration 속성에서 Style 속성을 ToolWindow 로 설정하면 나머지 디버그 세션 동안 열려 있을 수 있는 비모달 도구 창으로 시각화기가 열립니다. 선언된 스타일이 없으면 기본값 ModalDialog 이 사용됩니다.

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

시각화기가 ToolWindow로 열리도록 선택할 때마다 VisualizerTargetStateChanged 이벤트에 가입해야 합니다. 시각화 도우미를 도구 창으로 열면 사용자가 디버그 세션의 일시 중지를 차단하지 않습니다. 따라서 디버그 대상의 상태가 변경될 때마다 위에서 언급한 이벤트가 디버거에 의해 실행됩니다. 시각화 도우미 확장 작성자는 디버그 세션이 활성 상태이고 디버그 대상이 일시 중지된 경우에만 시각화 도우미 대상을 사용할 수 있기 때문에 이러한 알림에 특히 주의해야 합니다. 시각화 도우미 대상을 사용할 수 없는 경우 메서드 호출 ObjectSource 이 실패합니다 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");
            }
        }
    }
}

알림은 Available 새로 만든 시각화 도우미 도구 창에 표시되기 직전 및 생성된 후에 수신 RemoteUserControl 됩니다. 시각화 도우미가 다시 열려 있는 기본 디버그 대상이 상태를 변경할 때마다 다른 VisualizerTargetStateNotification 값을 받을 수 있습니다. 이 ValueUpdated 알림은 시각화 도우미에서 연 마지막 식이 디버거가 중지되고 UI에서 새로 고쳐야 하는 위치에서 성공적으로 다시 평가되었음을 나타내는 데 사용됩니다. 반면에 디버그 대상이 다시 시작되거나 중지 후 식을 다시 평가할 수 없을 때마다 알림이 Unavailable 수신됩니다.

시각화된 개체 값 업데이트

true이면 VisualizerTarget.IsTargetReplaceable 디버거 시각화 도우미는 ReplaceTargetObjectAsync 이 메서드를 사용하여 디버그 중인 프로세스에서 시각화된 개체의 값을 업데이트할 수 있습니다.

시각화 도우미 개체 원본은 CreateReplacementObject 메서드를 재정의해야 합니다.

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

샘플에서 RegexMatchDebugVisualizer 이러한 기법의 작동을 확인해 보세요.