Visual Studio デバッガーのビジュアライザーを作成する

デバッガー ビジュアライザーは、デバッグ セッション中に特定の .NET 型の変数またはオブジェクトのカスタム視覚化を提供する Visual Studio 機能です。

デバッガー ビジュアライザーには、変数の上にマウス ポインターを置いたときに表示されるデータヒントから、または [自動変数]、[ローカル]、[ウォッチ] ウィンドウからアクセスできます。

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 によって呼び出されます。 CreateVisualizerAsyncは、オブジェクトをVisualizerTarget使用して視覚化する値を取得し、それをカスタム リモート ユーザー コントロールに渡します (リモート UI ドキュメントを参照)。 その後、リモート ユーザー コントロールが返され、Visual Studio のポップアップ ウィンドウに表示されます。

複数の型をターゲットに設定する

構成プロパティを使用すると、ビジュアライザーは便利な場合に複数の型をターゲットにできます。 この完全な例として、オブジェクトの視覚化DataViewManagerDataViewDataTableDataSetをサポートする DataSet ビジュアライザーがあります。 この機能により、同様の型で同じ 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 によって自動的にシリアル化できない場合は、シリアル化を処理するカスタム ビジュアライザー オブジェクト ソースを作成できます。

  • ターゲットとする新しい .NET クラス ライブラリ プロジェクトを作成します netstandard2.0。 視覚化するオブジェクトをシリアル化するために必要な場合は、 net472 より具体的なバージョンの .NET Framework または net6.0.NET をターゲットにすることができます。
  • バージョン 17.6 以降への DebuggerVisualizers パッケージ参照を追加します。
  • ストリームへのシリアル化された値のVisualizerObjectSource書き込みを拡張およびオーバーライドGetDataするクラスをoutgoingData追加targetします。
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 を使用してオブジェクトを a にシリアル化できます。 SerializeAsJson呼び出すと、リフレクションを使用して、デバッグ中のプロセスに Newtonsoft.Json アセンブリのバージョンが読み込まれます。

Newtonsoft.Json を参照する必要がある場合は、パッケージによってMicrosoft.VisualStudio.Extensibility.Sdk参照されるのと同じバージョンを使用する必要がありますが、Newtonsoft.Json 型に依存するのではなく、オブジェクトのシリアル化をサポートするために使用およびDataMember属性を使用DataContractすることをお勧めします。

または、直接書き込む独自のカスタム シリアル化 (バイナリ シリアル化など) を outgoingData実装することもできます。

ビジュアライザー オブジェクト ソース DLL を拡張機能に追加する

ビジュアライザー オブジェクト ソース ライブラリ プロジェクトに追加ProjectReferenceする拡張.csprojファイルを変更します。これにより、拡張機能がパッケージ化される前にビジュアライザー オブジェクト ソース ライブラリがビルドされます。

また、 Content ビジュアライザー オブジェクト ソース ライブラリ DLL を含む項目を 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 または netcoreapp .NET をnet4.6.2対象とするビジュアライザー オブジェクト ソース ライブラリを構築した場合は、サブフォルダーを使用することもできます。 ビジュアライザー オブジェクト ソース ライブラリのバージョンが異なる 3 つのサブフォルダーをすべて含めることもできますが、ターゲットにすることをお勧めします 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);
    }

大きなオブジェクトと複雑なオブジェクトを操作する

ビジュアライザー オブジェクト ソースからデータを 1 回のパラメーターなしの呼び出しで取得できない場合は、ビジュアライザー オブジェクト ソースに複数回呼び出RequestDataAsyncして異なるメッセージをRequestDataAsync<TMessage, TResponse>(TMessage, JsonSerializer?, CancellationToken)送信することで、ビジュアライザー オブジェクト ソースとのより複雑なメッセージ交換を実行できます。 メッセージと応答の両方が、Newtonsoft.Json を使用して VisualStudio.Extensibility インフラストラクチャによってシリアル化されます。 その他の RequestDataAsync オーバーライドを使用すると、オブジェクトを使用 JToken したり、カスタムのシリアル化と逆シリアル化を実装したりできます。

さまざまなメッセージを使用して任意のカスタム プロトコルを実装して、ビジュアライザー オブジェクト ソースから情報を取得できます。 この機能の最も一般的なユース ケースは、タイムアウトを回避 RequestDataAsync するために、大規模な可能性があるオブジェクトの取得を複数の呼び出しに分割することです。

これは、大規模な可能性があるコレクションのコンテンツを一度に 1 つずつ取得する方法の例です。

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 使用しています。 対応するビジュアライザー オブジェクトのソース コードは、(ではなくGetData) メソッドをTransferDataオーバーライドします。

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 と対話する場合は、ビジュアライザーを閉じる必要があります。 ただし、プロパティがStyleプロパティにDebuggerVisualizerProviderConfiguration設定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くことを選択するたびに、VisualizerTarget. ビジュアライザーがツール ウィンドウとして開かれると、ユーザーがデバッグ セッションの一時解除をブロックされることはありません。 そのため、デバッグ ターゲットの状態が変化するたびに、前述のメンションイベントがデバッガーによって発生します。 ビジュアライザー拡張機能の作成者は、デバッグ セッションがアクティブでデバッグ ターゲットが一時停止されている場合にのみビジュアライザー ターゲットを使用できるため、これらの通知に特別な注意を払う必要があります。 ビジュアライザーターゲットが使用できない場合、メソッドの 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 試して、これらの手法の動作を確認してください。