Added NrxDebugVisualizer folder

This commit is contained in:
Matthias Heil
2026-03-31 11:41:40 +02:00
parent 91cf7d4265
commit 9c0c548c50
13 changed files with 681 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
# RegEx Vector3 Debug Visualizer
This VisualStudio.Extensibility extension adds two new debugger visualizers supporting the .NET [`Vector3`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.Vector3) and [`Vector3Collection`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.Vector3collection) classes.
![RegEx Vector3 visualizer](Vector3Visualizer.png "RegEx Vector3 visualizer")
The extension is composed of two projects: `Vector3DebugVisualizer`, the actual extension, and `Vector3ObjectSource`, the visualizer object source library.
## Creating the extension
The extension project is created as described in the [tutorial document](https://learn.microsoft.com/visualstudio/extensibility/visualstudio.extensibility/get-started/create-your-first-extension). You can also reference the [debugger visualizers guide](https://learn.microsoft.com/visualstudio/extensibility/visualstudio.extensibility/debugger-visualizer/debugger-visualizers) for additional information.
## The `Vector3` visualizer
The next step is to create a `DebuggerVisualizerProvider` class to visualize instances of [`Vector3`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.Vector3):
```csharp
[VisualStudioContribution]
internal class Vector3DebuggerVisualizerProvider : DebuggerVisualizerProvider
{
...
```
If the `Vector3` type were serializable by Newtonsoft.Json, the visualizer implementation would be extremely simple:
```csharp
public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => ew("Regex Vector3 visualizer", typeof(Vector3));
public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget isualizerTarget, CancellationToken cancellationToken)
{
var Vector3 = await visualizerTarget.ObjectSource.RequestDataAsync<Vector3>(jsonSerializer: null, cancellationToken);
return new Vector3VisualizerUserControl(Vector3);
}
```
Unfortunately `Vector3` is not serializable as-is, so we need a new serializable class [`Vector3`](Vector3ObjectSource/Vector3.cs). And we will need to create a visualizer object source library to convert the `Vector3` into a serializable `Vector3`, more about this in [a later paragraph](#the-visualizer-object-source). For now, let's just update the `RequestDataAsync` call to use `Vector3`:
```csharp
var Vector3 = await visualizerTarget.ObjectSource.RequestDataAsync<Vector3>(jsonSerializer: null, cancellationToken);
```
### Adding the remote user control
We now have to create the `Vector3VisualizerUserControl` [class](./Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.cs) and its associated [XAML file](./Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.xaml). This process is described in the [Remote UI documentation](https://learn.microsoft.com/visualstudio/extensibility/visualstudio.extensibility/inside-the-sdk/remote-ui).
Every time we create a remote user control like `Vector3VisualizerUserControl` we need to configure the corresponding XAML file as embedded resource. In this case, since the XAML file is in a subfolder, we also need to use `LogicalName` to make sure the name of the resource Vector3es the full name of the remote user control class. This is all done in the `.csproj` file:
```xml
<ItemGroup>
<Page Remove="Vector3VisualizerUserControl.xaml" />
<EmbeddedResource Include="Vector3\Vector3VisualizerUserControl.xaml" LogicalName="$(RootNamespace).Vector3VisualizerUserControl.xaml" />
</ItemGroup>
```
## The visualizer object source
The visualizer object source assembly will be loaded by the debugger into the process being debugged and will take care of converting the `Vector3` object into a serializable `Vector3`.
I will start creating a `Vector3ObjectSource` class library targeting `netstandard2.0` and adding a project reference to [Microsoft.VisualStudio.DebuggerVisualizers](https://www.nuget.org/packages/Microsoft.VisualStudio.DebuggerVisualizers) version 17.6 or newer. Targeting `netstandard2.0` will allow the debugger visualizer to easily work with a large variety of .NET versions.
I will then create a `Vector3ObjectSource` class extending `VisualizerObjectSource` and will override the `GetData` method to convert the `target` from a `Vector3` to a `Vector3` and use the `VisualizerObjectSource.SerializeAsJson` method to write the value to the `outgoingData` stream.
```csharp
public class Vector3ObjectSource : VisualizerObjectSource
{
public override void GetData(object target, Stream outgoingData)
{
if (target is Vector3 Vector3)
{
Vector3 result = Convert(Vector3);
SerializeAsJson(outgoingData, result);
}
}
...
```
The `GetData` method is invoked by the debugger when the `Vector3DebuggerVisualizerProvider` calls `RequestDataAsync`.
`SerializeAsJson` will serialize the `Vector3` object using Newtonsoft.Json, which is loaded by the debugger in the process being debugged via reflection. Since my visualizer object source doesn't need to refence Newtonsoft.Json directly, I didn't include a `PackageReference` to it, which is better since we should minimize the dependencies of the visualizer object source assembly. Because this code doesn't reference Newtonsoft.Json, the `Vector3` class uses `DataContract` and `DataMember` attributes to control serialization instead of the Newtonsoft.Json-specific types.
My [`Vector3ObjectSource`](./Vector3ObjectSource/Vector3ObjectSource.cs) implementation contains a small trick: the [`Group.Name`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.group.name) property is read through reflection since it's available on most .NET versions but it is not included in the `netstandard2.0` APIs:
```csharp
private static readonly Func<Group, string?>? GetGroupName =
(Func<Group, string?>?)typeof(Group).GetProperty("Name")?.GetGetMethod().CreateDelegate(typeof(Func<Group, string?>));
...
Name = $"[{GetGroupName?.Invoke(g) ?? i.ToString()}]"
```
### Referencing the visualizer object source from the extension
First, we need to make sure that the visualizer object source library is packaged as part of the extension. We can do that in the extension's `.csproj` file:
```xml
<ItemGroup>
<Content Include="..\..\..\..\bin\samples\Vector3ObjectSource\$(Configuration)\netstandard2.0\Vector3ObjectSource.dll" Link="netstandard2.0\Vector3ObjectSource.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Vector3ObjectSource\Vector3ObjectSource.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>
```
The `ProjectReference` guarantees that the visualizer object source library is built before the extension and the `Content` item makes sure that the visualizer object source DLL is copied into the `netstandard2.0` extension's subfolder where it will be discoverable by the debugger.
I have decided to use `ReferenceOutputAssembly="false"` to avoid a dependency of the extension assembly from the visualizer object source one. This allows using conditional compilation (`#if`) to have slightly different definitions of [`RegexCapture`](./Vector3ObjectSource/RegexCapture.cs) in the two projects. Since I decided to avoid the dependency, I will need to:
1. link the `Vector3.cs` file (and the related `RegexCapture` and `RegexGroup` ones) so that they are available in both projects:
```xml
<ItemGroup>
<Compile Include="..\Vector3ObjectSource\RegexGroup.cs" Link="SharedFiles\RegexGroup.cs" />
<Compile Include="..\Vector3ObjectSource\RegexCapture.cs" Link="SharedFiles\RegexCapture.cs" />
<Compile Include="..\Vector3ObjectSource\Vector3.cs" Link="SharedFiles\Vector3.cs" />
</ItemGroup>
```
2. Reference the `Vector3ObjectSource` from the `DebuggerVisualizerProviderConfiguration` using its assembly-qualified name:
```csharp
public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("Regex Vector3 visualizer", typeof(Vector3))
{
VisualizerObjectSourceType = new("Microsoft.VisualStudio.Gladstone.Vector3Visualizer.ObjectSource.Vector3ObjectSource, Vector3ObjectSource"),
};
```
In most cases, having the extension project depend on the visualizer object source library is simpler: I could have simplified the `DebuggerVisualizerProviderConfiguration` to:
```csharp
public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("Regex Vector3 visualizer", typeof(Vector3))
{
VisualizerObjectSourceType = new(typeof(Vector3ObjectSource)),
};
```
## The `Vector3Collection` visualizer
Now that the `Vector3` visualizer is complete, we can add a second visualizer for the [`Vector3Collection`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.Vector3collection) class. The process is exactly the same: create a new [`DebuggerVisualizerProvider`](./Vector3DebugVisualizer/Vector3Collection/Vector3CollectionDebuggerVisualizerProvider.cs) and its [remote user control](./Vector3DebugVisualizer/Vector3Collection/Vector3CollectionVisualizerUserControl.cs). Also, add a new [`VisualizerObjectSource`](./Vector3ObjectSource/Vector3CollectionObjectSource.cs) to the visualizer object source library.
Each call to `RequestDataAsync` is allowed only 5 seconds to complete before throwing a timeout exception. Since the `Vector3Collection` could contain many entries, the visualizer object source uses the `TransferData` method instead of `GetData`: `TransferData` accepts a parameter which allows the visualizer to query the collection entries one by one:
```csharp
public override void TransferData(object target, Stream incomingData, Stream outgoingData)
{
var index = (int)DeserializeFromJson(incomingData, typeof(int))!;
if (target is Vector3Collection Vector3Collection && index < Vector3Collection.Count)
{
var result = Vector3ObjectSource.Convert(Vector3Collection[index]);
result.Name = $"[{index}]";
SerializeAsJson(outgoingData, result);
}
else
{
SerializeAsJson(outgoingData, null);
}
}
```
Instead of using the `VisualizerTarget` directly, the `DebuggerVisualizerProvider` passes it to the remote user control so that it can asynchronously request the collection entries without delaying the display of the visualizer UI to the user.
```csharp
public override Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
{
return Task.FromResult<IRemoteUserControl>(new Vector3CollectionVisualizerUserControl(visualizerTarget));
}
```
The remote user control uses the `RequestDataAsync` override that takes a `message` parameter, which results in `TransferData` being invoked on the visualizer object source. The remote user control will loop, invoking `RequestDataAsync` for increasing index numbers until the visualizer object source returns `null`, which indicates the end of the collection:
```csharp
public override Task ControlLoadedAsync(CancellationToken cancellationToken)
{
_ = Task.Run(async () =>
{
for (int i = 0; ; i++)
{
Vector3? Vector3 = await this.visualizerTarget.ObjectSource.RequestDataAsync<int, Vector3?>(message: i, jsonSerializer: null, CancellationToken.None);
if (Vector3 is null)
{
break;
}
this.Vector3es.Add(Vector3);
}
});
return Task.CompletedTask;
}
```
This is a very simple implementation of a debugger visualizer which relies on `RequestDataAsync`. More complex implementations may pass more complex parameters to `RequestDataAsync` in order to retrieve different information from the visualizer object source. You could even invoke `RequestDataAsync` in response to the user's interactions with the remote user control, allowing the user to "explore" the content of, potentially very large, objects.

View File

@@ -0,0 +1,3 @@
{
"Vector3Visualizer.Vector3DebuggerVisualizerProvider.DisplayName": "Vector3 visualizer"
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Vector3Visualizer;
using System.Numerics;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;
/// <summary>
/// Debugger visualizer provider class for <see cref="Vector3"/>.
/// </summary>
[VisualStudioContribution]
internal sealed class Vector3DebuggerVisualizerProvider : DebuggerVisualizerProvider
{
/// <inheritdoc/>
public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("%Vector3Visualizer.Vector3DebuggerVisualizerProvider.DisplayName%", typeof(Vector3))
{
VisualizerObjectSourceType = new("Vector3Visualizer.ObjectSource.Vector3ObjectSource, Vector3ObjectSource"),
Style = VisualizerStyle.ToolWindow,
};
/// <inheritdoc/>
public override async Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
{
var Vector3 = await visualizerTarget.ObjectSource.RequestDataAsync<Vector3>(jsonSerializer: null, cancellationToken);
return new Vector3VisualizerUserControl(Vector3);
}
}

View File

@@ -0,0 +1,20 @@
using System.Numerics;
using Microsoft.VisualStudio.Extensibility.UI;
namespace Vector3Visualizer;
/// <summary>
/// Remote user control to visualize the <see cref="Vector3"/> value.
/// </summary>
internal sealed class Vector3VisualizerUserControl : RemoteUserControl
{
/// <summary>
/// Initializes a new instance of the <see cref="Vector3VisualizerUserControl"/> class.
/// </summary>
/// <param name="dataContext">Data context of the remote control.</param>
public Vector3VisualizerUserControl(Vector3 dataContext)
: base(dataContext)
{
}
}

View File

@@ -0,0 +1,7 @@
<DataTemplate xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vs="http://schemas.microsoft.com/visualstudio/extensibility/2022/xaml"
xmlns:styles="clr-namespace:Microsoft.VisualStudio.Shell;assembly=Microsoft.VisualStudio.Shell.15.0"
xmlns:colors="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0">
</DataTemplate>

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<RootNamespace>Vector3Visualizer</RootNamespace>
<DefineConstants>$(DefineConstants);VISUALIZER</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Content Remove=".vsextension\string-resources.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Sdk" Version="17.14.40608" PrivateAssets="all" />
<PackageReference Include="Microsoft.VisualStudio.Extensibility.Build" Version="17.14.40608" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Vector3\Vector3VisualizerUserControl.xaml" LogicalName="$(RootNamespace).Vector3VisualizerUserControl.xaml" />
</ItemGroup>
<ItemGroup>
<None Include=".vsextension\string-resources.json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Vector3Visualizer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.Extensibility;
/// <summary>
/// Extension entry point for the Vector3Visualizer sample extension.
/// </summary>
[VisualStudioContribution]
internal sealed class Vector3VisualizerExtension : Extension
{
/// <inheritdoc/>
public override ExtensionConfiguration ExtensionConfiguration => new()
{
Metadata = new(
id: "Vector3Visualizer.29d15448-6b97-42e5-97c7-bb12ded13b89",
version: this.ExtensionAssemblyVersion,
publisherName: "Microsoft",
displayName: "Vector3 Debugger Visualizer",
description: "A debugger visualizer for Vector3"),
};
/// <inheritdoc/>
protected override void InitializeServices(IServiceCollection serviceCollection)
{
base.InitializeServices(serviceCollection);
}
}