From 9c0c548c50d50240559992555f9f112f24ea93d9 Mon Sep 17 00:00:00 2001 From: Matthias Heil Date: Tue, 31 Mar 2026 11:41:40 +0200 Subject: [PATCH] Added NrxDebugVisualizer folder --- NrxDebugVisualizer/Directory.Build.props | 51 +++++ NrxDebugVisualizer/NrxDebugVisualizer.sln | 32 +++ NrxDebugVisualizer/README.md | 196 ++++++++++++++++++ .../TestDebugVisualizer/Program.cs | 49 +++++ .../TestDebugVisualizer.csproj | 10 + NrxDebugVisualizer/TestVisualizer.sln | 25 +++ .../Vector3DebugVisualizer/README.md | 196 ++++++++++++++++++ .../.vsextension/string-resources.json | 3 + .../Vector3DebuggerVisualizerProvider.cs | 35 ++++ .../Vector3/Vector3VisualizerUserControl.cs | 20 ++ .../Vector3/Vector3VisualizerUserControl.xaml | 7 + .../Vector3DebugVisualizer.csproj | 25 +++ .../Vector3VisualizerExtension.cs | 32 +++ 13 files changed, 681 insertions(+) create mode 100644 NrxDebugVisualizer/Directory.Build.props create mode 100644 NrxDebugVisualizer/NrxDebugVisualizer.sln create mode 100644 NrxDebugVisualizer/README.md create mode 100644 NrxDebugVisualizer/TestDebugVisualizer/Program.cs create mode 100644 NrxDebugVisualizer/TestDebugVisualizer/TestDebugVisualizer.csproj create mode 100644 NrxDebugVisualizer/TestVisualizer.sln create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/README.md create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/.vsextension/string-resources.json create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3DebuggerVisualizerProvider.cs create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.cs create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.xaml create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer.csproj create mode 100644 NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3VisualizerExtension.cs diff --git a/NrxDebugVisualizer/Directory.Build.props b/NrxDebugVisualizer/Directory.Build.props new file mode 100644 index 0000000..a77b037 --- /dev/null +++ b/NrxDebugVisualizer/Directory.Build.props @@ -0,0 +1,51 @@ + + + Debug + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\..\')) + $(RepoRootPath)obj\samples\$(MSBuildProjectName)\ + $(RepoRootPath)bin\samples\$(MSBuildProjectName)\ + false + false + true + $(MSBuildThisFileDirectory)shipping.ruleset + true + true + latest + + + + + true + $(NoWarn);SA1600;SA1602;CS1591 + + + $(NoWarn);SA1010 + + + $(NoWarn);CA1812;CA1303 + + + + + + + + + + diff --git a/NrxDebugVisualizer/NrxDebugVisualizer.sln b/NrxDebugVisualizer/NrxDebugVisualizer.sln new file mode 100644 index 0000000..7ceb48e --- /dev/null +++ b/NrxDebugVisualizer/NrxDebugVisualizer.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11620.152 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{070F0AEA-C0A0-4B5D-9286-55574A37BE7A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Vector3DebugVisualizer", "Vector3DebugVisualizer", "{8554ABE3-9105-4AF7-9318-81414CC190C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vector3DebugVisualizer", "Vector3DebugVisualizer\Vector3DebugVisualizer\Vector3DebugVisualizer.csproj", "{D8310D46-26AC-C7D7-1668-BFEBB7072467}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D8310D46-26AC-C7D7-1668-BFEBB7072467}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8310D46-26AC-C7D7-1668-BFEBB7072467}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8310D46-26AC-C7D7-1668-BFEBB7072467}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8310D46-26AC-C7D7-1668-BFEBB7072467}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D8310D46-26AC-C7D7-1668-BFEBB7072467} = {8554ABE3-9105-4AF7-9318-81414CC190C6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {40A38C8A-61B7-427B-A430-DEB75BE34F22} + EndGlobalSection +EndGlobal diff --git a/NrxDebugVisualizer/README.md b/NrxDebugVisualizer/README.md new file mode 100644 index 0000000..415ff60 --- /dev/null +++ b/NrxDebugVisualizer/README.md @@ -0,0 +1,196 @@ +# RegEx Match Debug Visualizer + +This VisualStudio.Extensibility extension adds two new debugger visualizers supporting the .NET [`Match`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.match) and [`MatchCollection`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.matchcollection) classes. + +![RegEx Match visualizer](RegexMatchVisualizer.png "RegEx Match visualizer") + +The extension is composed of two projects: `RegexMatchDebugVisualizer`, the actual extension, and `RegexMatchObjectSource`, 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 `Match` visualizer + +The next step is to create a `DebuggerVisualizerProvider` class to visualize instances of [`Match`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.match): + +```csharp +[VisualStudioContribution] +internal class RegexMatchDebuggerVisualizerProvider : DebuggerVisualizerProvider +{ + ... +``` + +If the `Match` type were serializable by Newtonsoft.Json, the visualizer implementation would be extremely simple: + +```csharp +public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => ew("Regex Match visualizer", typeof(Match)); + +public override async Task CreateVisualizerAsync(VisualizerTarget isualizerTarget, CancellationToken cancellationToken) +{ + var regexMatch = await visualizerTarget.ObjectSource.RequestDataAsync(jsonSerializer: null, cancellationToken); + return new RegexMatchVisualizerUserControl(regexMatch); +} +``` + +Unfortunately `Match` is not serializable as-is, so we need a new serializable class [`RegexMatch`](RegexMatchObjectSource/RegexMatch.cs). And we will need to create a visualizer object source library to convert the `Match` into a serializable `RegexMatch`, more about this in [a later paragraph](#the-visualizer-object-source). For now, let's just update the `RequestDataAsync` call to use `RegexMatch`: + +```csharp +var regexMatch = await visualizerTarget.ObjectSource.RequestDataAsync(jsonSerializer: null, cancellationToken); +``` + +### Adding the remote user control + +We now have to create the `RegexMatchVisualizerUserControl` [class](./RegexMatchDebugVisualizer/RegexMatch/RegexMatchVisualizerUserControl.cs) and its associated [XAML file](./RegexMatchDebugVisualizer/RegexMatch/RegexMatchVisualizerUserControl.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 `RegexMatchVisualizerUserControl` 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 matches the full name of the remote user control class. This is all done in the `.csproj` file: + +```xml + + + + +``` + +## 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 `Match` object into a serializable `RegexMatch`. + +I will start creating a `RegexMatchObjectSource` 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 `RegexMatchObjectSource` class extending `VisualizerObjectSource` and will override the `GetData` method to convert the `target` from a `Match` to a `RegexMatch` and use the `VisualizerObjectSource.SerializeAsJson` method to write the value to the `outgoingData` stream. + +```csharp +public class RegexMatchObjectSource : VisualizerObjectSource +{ + public override void GetData(object target, Stream outgoingData) + { + if (target is Match match) + { + RegexMatch result = Convert(match); + SerializeAsJson(outgoingData, result); + } + } + + ... +``` + +The `GetData` method is invoked by the debugger when the `RegexMatchDebuggerVisualizerProvider` calls `RequestDataAsync`. + +`SerializeAsJson` will serialize the `RegexMatch` 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 `RegexMatch` class uses `DataContract` and `DataMember` attributes to control serialization instead of the Newtonsoft.Json-specific types. + +My [`RegexMatchObjectSource`](./RegexMatchObjectSource/RegexMatchObjectSource.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? GetGroupName = + (Func?)typeof(Group).GetProperty("Name")?.GetGetMethod().CreateDelegate(typeof(Func)); + +... + +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 + + + PreserveNewest + + + + + + +``` + +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`](./RegexMatchObjectSource/RegexCapture.cs) in the two projects. Since I decided to avoid the dependency, I will need to: + +1. link the `RegexMatch.cs` file (and the related `RegexCapture` and `RegexGroup` ones) so that they are available in both projects: + +```xml + + + + + +``` + +2. Reference the `RegexMatchObjectSource` from the `DebuggerVisualizerProviderConfiguration` using its assembly-qualified name: + +```csharp + public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("Regex Match visualizer", typeof(Match)) + { + VisualizerObjectSourceType = new("Microsoft.VisualStudio.Gladstone.RegexMatchVisualizer.ObjectSource.RegexMatchObjectSource, RegexMatchObjectSource"), + }; +``` + +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 Match visualizer", typeof(Match)) + { + VisualizerObjectSourceType = new(typeof(RegexMatchObjectSource)), + }; +``` + +## The `MatchCollection` visualizer + +Now that the `Match` visualizer is complete, we can add a second visualizer for the [`MatchCollection`](https://learn.microsoft.com/dotnet/api/system.text.regularexpressions.matchcollection) class. The process is exactly the same: create a new [`DebuggerVisualizerProvider`](./RegexMatchDebugVisualizer/RegexMatchCollection/RegexMatchCollectionDebuggerVisualizerProvider.cs) and its [remote user control](./RegexMatchDebugVisualizer/RegexMatchCollection/RegexMatchCollectionVisualizerUserControl.cs). Also, add a new [`VisualizerObjectSource`](./RegexMatchObjectSource/RegexMatchCollectionObjectSource.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 `MatchCollection` 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 MatchCollection matchCollection && index < matchCollection.Count) + { + var result = RegexMatchObjectSource.Convert(matchCollection[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 CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken) +{ + return Task.FromResult(new RegexMatchCollectionVisualizerUserControl(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++) + { + RegexMatch? regexMatch = await this.visualizerTarget.ObjectSource.RequestDataAsync(message: i, jsonSerializer: null, CancellationToken.None); + if (regexMatch is null) + { + break; + } + + this.RegexMatches.Add(regexMatch); + } + }); + + 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. diff --git a/NrxDebugVisualizer/TestDebugVisualizer/Program.cs b/NrxDebugVisualizer/TestDebugVisualizer/Program.cs new file mode 100644 index 0000000..90bbe7f --- /dev/null +++ b/NrxDebugVisualizer/TestDebugVisualizer/Program.cs @@ -0,0 +1,49 @@ +using System.Text.RegularExpressions; + +class Example +{ + static void Main() + { + string text = "One car red car blue car"; + string pat = @"(\w+)\s+(car)"; + + // Instantiate the regular expression object. + Regex r = new Regex(pat, RegexOptions.IgnoreCase); + + // Match the regular expression pattern against a text string. + Match m = r.Match(text); + int matchCount = 0; + while (m.Success) + { + Console.WriteLine("Match" + (++matchCount)); + for (int i = 1; i <= 2; i++) + { + Group g = m.Groups[i]; + Console.WriteLine("Group" + i + "='" + g + "'"); + CaptureCollection cc = g.Captures; + for (int j = 0; j < cc.Count; j++) + { + Capture c = cc[j]; + System.Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index); + } + } + m = m.NextMatch(); + } + } +} +// This example displays the following output: +// Match1 +// Group1='One' +// Capture0='One', Position=0 +// Group2='car' +// Capture0='car', Position=4 +// Match2 +// Group1='red' +// Capture0='red', Position=8 +// Group2='car' +// Capture0='car', Position=12 +// Match3 +// Group1='blue' +// Capture0='blue', Position=16 +// Group2='car' +// Capture0='car', Position=21 \ No newline at end of file diff --git a/NrxDebugVisualizer/TestDebugVisualizer/TestDebugVisualizer.csproj b/NrxDebugVisualizer/TestDebugVisualizer/TestDebugVisualizer.csproj new file mode 100644 index 0000000..ed9781c --- /dev/null +++ b/NrxDebugVisualizer/TestDebugVisualizer/TestDebugVisualizer.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/NrxDebugVisualizer/TestVisualizer.sln b/NrxDebugVisualizer/TestVisualizer.sln new file mode 100644 index 0000000..d4c293a --- /dev/null +++ b/NrxDebugVisualizer/TestVisualizer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11620.152 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestDebugVisualizer", "TestDebugVisualizer\TestDebugVisualizer.csproj", "{221A5163-F586-4766-A524-C8097AD06068}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {221A5163-F586-4766-A524-C8097AD06068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {221A5163-F586-4766-A524-C8097AD06068}.Debug|Any CPU.Build.0 = Debug|Any CPU + {221A5163-F586-4766-A524-C8097AD06068}.Release|Any CPU.ActiveCfg = Release|Any CPU + {221A5163-F586-4766-A524-C8097AD06068}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C63DCA03-00AC-476D-B4FE-43DC4F4EBA35} + EndGlobalSection +EndGlobal diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/README.md b/NrxDebugVisualizer/Vector3DebugVisualizer/README.md new file mode 100644 index 0000000..180c4d6 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/README.md @@ -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 CreateVisualizerAsync(VisualizerTarget isualizerTarget, CancellationToken cancellationToken) +{ + var Vector3 = await visualizerTarget.ObjectSource.RequestDataAsync(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(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 + + + + +``` + +## 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? GetGroupName = + (Func?)typeof(Group).GetProperty("Name")?.GetGetMethod().CreateDelegate(typeof(Func)); + +... + +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 + + + PreserveNewest + + + + + + +``` + +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 + + + + + +``` + +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 CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken) +{ + return Task.FromResult(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(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. diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/.vsextension/string-resources.json b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/.vsextension/string-resources.json new file mode 100644 index 0000000..75bf520 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/.vsextension/string-resources.json @@ -0,0 +1,3 @@ +{ + "Vector3Visualizer.Vector3DebuggerVisualizerProvider.DisplayName": "Vector3 visualizer" +} \ No newline at end of file diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3DebuggerVisualizerProvider.cs b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3DebuggerVisualizerProvider.cs new file mode 100644 index 0000000..7c6aef3 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3DebuggerVisualizerProvider.cs @@ -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; + + +/// +/// Debugger visualizer provider class for . +/// +[VisualStudioContribution] +internal sealed class Vector3DebuggerVisualizerProvider : DebuggerVisualizerProvider +{ + /// + public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new("%Vector3Visualizer.Vector3DebuggerVisualizerProvider.DisplayName%", typeof(Vector3)) + { + VisualizerObjectSourceType = new("Vector3Visualizer.ObjectSource.Vector3ObjectSource, Vector3ObjectSource"), + Style = VisualizerStyle.ToolWindow, + }; + + /// + public override async Task CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken) + { + var Vector3 = await visualizerTarget.ObjectSource.RequestDataAsync(jsonSerializer: null, cancellationToken); + + return new Vector3VisualizerUserControl(Vector3); + } +} diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.cs b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.cs new file mode 100644 index 0000000..9ede3e8 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Microsoft.VisualStudio.Extensibility.UI; + +namespace Vector3Visualizer; + + +/// +/// Remote user control to visualize the value. +/// +internal sealed class Vector3VisualizerUserControl : RemoteUserControl +{ + /// + /// Initializes a new instance of the class. + /// + /// Data context of the remote control. + public Vector3VisualizerUserControl(Vector3 dataContext) + : base(dataContext) + { + } +} diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.xaml b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.xaml new file mode 100644 index 0000000..a1ccbb4 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3/Vector3VisualizerUserControl.xaml @@ -0,0 +1,7 @@ + + + diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer.csproj b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer.csproj new file mode 100644 index 0000000..c1d4836 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer.csproj @@ -0,0 +1,25 @@ + + + + net8.0-windows8.0 + enable + latest + Vector3Visualizer + $(DefineConstants);VISUALIZER + + + + + + + + + + + + + + + + + diff --git a/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3VisualizerExtension.cs b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3VisualizerExtension.cs new file mode 100644 index 0000000..d6abc50 --- /dev/null +++ b/NrxDebugVisualizer/Vector3DebugVisualizer/Vector3DebugVisualizer/Vector3VisualizerExtension.cs @@ -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; + +/// +/// Extension entry point for the Vector3Visualizer sample extension. +/// +[VisualStudioContribution] +internal sealed class Vector3VisualizerExtension : Extension +{ + /// + 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"), + }; + + /// + protected override void InitializeServices(IServiceCollection serviceCollection) + { + base.InitializeServices(serviceCollection); + + } +}