次の方法で共有


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

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

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

ウォッチ ウィンドウのデバッガー ビジュアライザーのスクリーンショット。

概要

[作業の開始] セクション の [拡張機能プロジェクトの作成 ] セクションに従います。

次に、 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 のポップアップ ウィンドウに表示されます。

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

構成プロパティを使用すると、ビジュアライザーは便利な場合に複数の型をターゲットにできます。 この完全な例として、DataSetDataTableDataView オブジェクトの視覚化をサポートする DataViewManagerがあります。 この機能により、同様の型で同じ 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 ( net472net6.0など) を対象にすることができます。
  • DebuggerVisualizers バージョン 17.6 以降へのパッケージ参照を追加します。
  • VisualizerObjectSourceを拡張するクラスを追加し、GetDataをオーバーライドしてtargetのシリアル化された値を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
        ...
    }
}

カスタム シリアル化を使用する

VisualizerObjectSource.SerializeAsJsonメソッドを使用すると、Newtonsoft.Json への参照をライブラリに追加せずに、Newtonsoft.Json を使用してオブジェクトをStreamにシリアル化できます。 SerializeAsJsonを呼び出すと、リフレクションを使用して、デバッグ中のプロセスに Newtonsoft.Json アセンブリのバージョンが読み込まれます。

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

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

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

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

また、ビジュアライザー オブジェクト ソース ライブラリ 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 を対象とするビジュアライザー オブジェクト ソース ライブラリをビルドした場合は、 net4.6.2 または netcoreapp サブフォルダーを使用することもできます。 ビジュアライザー オブジェクト ソース ライブラリのバージョンが異なる 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);
    }

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

ビジュアライザー オブジェクト ソースからのデータの取得を、 RequestDataAsyncへの 1 回のパラメーターなしの呼び出しで実行できない場合は、代わりに、 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 呼び出しのメッセージとして単純なインデックスを使用しています。 対応するビジュアライザー オブジェクトのソース コードは、(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 と対話する場合は、ビジュアライザーを閉じる必要があります。 ただし、Style プロパティが ToolWindow プロパティでDebuggerVisualizerProviderConfigurationに設定されている場合、ビジュアライザーは、デバッグ セッションの残りの部分で開いたままにできる非モーダル ツール ウィンドウとして開かれます。 スタイルが宣言されていない場合は、既定値 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 通知が受信されます。

視覚化されたオブジェクト値を更新する

VisualizerTarget.IsTargetReplaceableが true の場合、デバッガー ビジュアライザーは、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サンプルを試して、これらの手法の動作を確認してください。