Added MemoryStreamDebugVisualizer

This commit is contained in:
Matthias Heil
2026-01-13 11:40:39 +01:00
parent 38246e4fe8
commit e48d6a63f8
13 changed files with 626 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="MemoryStreamObjectSource/MemoryStreamObjectSource.csproj" />
<Project Path="MemoryStreamVisualizer/MemoryStreamVisualizer.csproj" />
<Project Path="TestApp/TestApp.csproj" />
</Solution>

View File

@@ -0,0 +1,14 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<RepoRootPath>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))</RepoRootPath>
<BaseIntermediateOutputPath>$(RepoRootPath)obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
<BaseOutputPath Condition=" '$(BaseOutputPath)' == '' ">$(RepoRootPath)bin\$(MSBuildProjectName)\</BaseOutputPath>
<SignAssembly>false</SignAssembly>
<IsPackable>false</IsPackable>
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
<EnforceCodeStyleInBuild>false</EnforceCodeStyleInBuild>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="C:\Users\heilm\source\repos\DebugVisualizer\stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.DebuggerVisualizers" Version="17.6.1032901" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,64 @@
// <copyright file="MemoryStreamVisualizerObjectSource.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace MemoryStreamObjectSource;
using Microsoft.VisualStudio.DebuggerVisualizers;
using System.IO;
/// <summary>
/// Object source class for the MemoryStreamVisualizer.
/// </summary>
public class MemoryStreamVisualizerObjectSource : VisualizerObjectSource
{
/// <summary>
/// How many rows will be transfered, at most, responding to a single request.
/// </summary>
public const int RowsCountPerRequest = 1024;
/// <summary>
/// How many bytes will be transfered for each row.
/// </summary>
public const int RowLength = 16;
/// <inheritdoc/>
public override void TransferData(object target, Stream incomingData, Stream outgoingData)
{
if (target is not MemoryStream memoryStream)
{
return;
}
using BinaryReader binaryReader = new(incomingData);
var index = binaryReader.ReadInt32(); // The extension will send the offset (Int32) to start reading from
using BinaryWriter binaryWriter = new(outgoingData);
var backupPosition = memoryStream.Position;
// Will reply with the current MemoryStream.Position (Int64),
// followed by MemoryStream.Length (Int64),
// followed by up to 16KB of data retrieved from the MemoryStream
binaryWriter.Write(backupPosition);
binaryWriter.Write(memoryStream.Length);
if (index < memoryStream.Length)
{
try
{
var data = new byte[RowsCountPerRequest * RowLength];
memoryStream.Seek(index, SeekOrigin.Begin);
var count = memoryStream.Read(data, 0, data.Length);
binaryWriter.Write(data, 0, count);
}
finally
{
// Make sure to restore the MemoryStream to its original position
memoryStream.Seek(backupPosition, SeekOrigin.Begin);
}
}
binaryWriter.Flush();
}
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace MemoryStreamVisualizer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.Extensibility;
/// <summary>
/// Extension entrypoint for the VisualStudio.Extensibility extension.
/// </summary>
[VisualStudioContribution]
internal class ExtensionEntrypoint : Extension
{
/// <inheritdoc/>
public override ExtensionConfiguration ExtensionConfiguration => new()
{
Metadata = new(
id: "MemoryStreamVisualizer.97a0a2fb-f163-4fa3-91f0-48a2d4ad9f57",
version: this.ExtensionAssemblyVersion,
publisherName: "Microsoft",
displayName: "MemoryStream Debugger Visualizer",
description: "A debugger visualizer for MemoryStream"),
};
/// <inheritdoc />
protected override void InitializeServices(IServiceCollection serviceCollection)
{
base.InitializeServices(serviceCollection);
// You can configure dependency injection here by adding services to the serviceCollection.
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace MemoryStreamVisualizer;
using System.Runtime.Serialization;
/// <summary>
/// ViewModel class representing a row of binary data.
/// </summary>
[DataContract]
public class HexEditorRow
{
/// <summary>
/// Initializes a new instance of the <see cref="HexEditorRow"/> class.
/// </summary>
/// <param name="index">The index of this row.</param>
/// <param name="data">The bytes making up this row of data, in their hex representation.</param>
/// <param name="ascii">The bytes making up this row of data, in their Ascii representation.</param>
public HexEditorRow(int index, string data, string ascii)
{
this.Index = index;
this.Data = data;
this.Ascii = ascii;
}
/// <summary>
/// Gets the index of this row.
/// </summary>
[DataMember]
public int Index { get; }
/// <summary>
/// Gets the index of this row in hex format.
/// </summary>
[DataMember]
public string HexIndex => $"{this.Index:X8}h";
/// <summary>
/// Gets the bytes making up this row of data, in their hex representation.
/// </summary>
[DataMember]
public string Data { get; }
/// <summary>
/// Gets the bytes making up this row of data, in their Ascii representation.
/// </summary>
[DataMember]
public string Ascii { get; }
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace MemoryStreamVisualizer;
using System.IO;
using System.Runtime.Serialization;
using System.Windows;
using Microsoft.VisualStudio.Extensibility.UI;
/// <summary>
/// ViewModel class representing the data contained by a <see cref="MemoryStream"/>.
/// </summary>
[DataContract]
public class MemoryStreamData : NotifyPropertyChangedObject
{
private long length;
private long position;
private Visibility loadingVisibility = Visibility.Visible;
/// <summary>
/// Gets or sets the length of the <see cref="MemoryStream"/>.
/// </summary>
[DataMember]
public long Length
{
get => this.length;
set
{
if (this.SetProperty(ref this.length, value))
{
this.RaiseNotifyPropertyChangedEvent(nameof(this.HexLength));
}
}
}
/// <summary>
/// Gets the length of the <see cref="MemoryStream"/> in hex format.
/// </summary>
[DataMember]
public string HexLength => $"{this.length:X}h";
/// <summary>
/// Gets or sets the current position of the <see cref="MemoryStream"/>.
/// </summary>
[DataMember]
public long Position
{
get => this.position;
set
{
if (this.SetProperty(ref this.position, value))
{
this.RaiseNotifyPropertyChangedEvent(nameof(this.HexPosition));
}
}
}
/// <summary>
/// Gets the current position of the <see cref="MemoryStream"/> in hex format.
/// </summary>
[DataMember]
public string HexPosition => $"{this.position:X}h";
/// <summary>
/// Gets the data currently contained in the <see cref="MemoryStream"/>.
/// </summary>
[DataMember]
public ObservableList<HexEditorRow> Data { get; } = new();
/// <summary>
/// Gets or sets whether the loading bar should be visible.
/// </summary>
[DataMember]
public Visibility LoadingVisibility
{
get => this.loadingVisibility;
set => this.SetProperty(ref this.loadingVisibility, value);
}
}

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 MemoryStreamVisualizer;
using MemoryStreamObjectSource;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.RpcContracts.RemoteUI;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// Debugger visualizer provider for <see cref="MemoryStream"/>.
/// </summary>
[VisualStudioContribution]
internal class MemoryStreamDebuggerVisualizerProvider : DebuggerVisualizerProvider
{
/// <inheritdoc/>
public override DebuggerVisualizerProviderConfiguration DebuggerVisualizerProviderConfiguration => new(
[
new VisualizerTargetType("%MemoryStreamVisualizer.MemoryStreamDebuggerVisualizerProvider.Name%", typeof(MemoryStream))
])
{
VisualizerObjectSourceType = new VisualizerObjectSourceType(typeof(MemoryStreamVisualizerObjectSource)),
Style = VisualizerStyle.ToolWindow,
};
/// <inheritdoc/>
public override Task<IRemoteUserControl> CreateVisualizerAsync(VisualizerTarget visualizerTarget, CancellationToken cancellationToken)
{
return Task.FromResult<IRemoteUserControl>(new MemoryStreamVisualizerUserControl(visualizerTarget));
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<NeutralLanguage>en-US</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="C:\Users\heilm\source\repos\DebugVisualizer\stylecop.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>
<None Remove="MemoryStreamVisualizerUserControl.xaml" />
<EmbeddedResource Include="MemoryStreamVisualizerUserControl.xaml" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MemoryStreamObjectSource\MemoryStreamObjectSource.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\bin\MemoryStreamObjectSource\$(Configuration)\netstandard2.0\MemoryStreamObjectSource.dll" Link="netstandard2.0\MemoryStreamObjectSource.dll" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,150 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using MemoryStreamObjectSource;
using Microsoft.VisualStudio.Extensibility.DebuggerVisualizers;
using Microsoft.VisualStudio.Extensibility.UI;
using Microsoft.VisualStudio.RpcContracts.DebuggerVisualizers;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace MemoryStreamVisualizer;
/// <summary>
/// Remote UI user control for the MemoryStreamVisualizer.
/// </summary>
internal class MemoryStreamVisualizerUserControl : RemoteUserControl
{
private readonly VisualizerTarget visualizerTarget;
private readonly MemoryStreamData dataContext;
/// <summary>
/// Initializes a new instance of the <see cref="MemoryStreamVisualizerUserControl"/> class.
/// </summary>
/// <param name="visualizerTarget">The visualizer target to be used to retrieve data from.</param>
public MemoryStreamVisualizerUserControl(VisualizerTarget visualizerTarget)
: base(new MemoryStreamData())
{
visualizerTarget.Changed += this.VisualizerTargetStateChangedAsync;
this.dataContext = (MemoryStreamData)this.DataContext!;
this.visualizerTarget = visualizerTarget;
}
private Task VisualizerTargetStateChangedAsync(VisualizerTargetStateNotification args)
{
if (args == VisualizerTargetStateNotification.Available || args == VisualizerTargetStateNotification.ValueUpdated)
{
this.dataContext.Data.Clear();
this.dataContext.Position = 0;
this.dataContext.Length = 0;
this.dataContext.LoadingVisibility = Visibility.Visible;
return this.RetrieveDataAsync();
}
return Task.CompletedTask;
}
private async Task RetrieveDataAsync()
{
ReadOnlySequence<byte> data;
do
{
using MemoryStream memoryStream = new(sizeof(int));
using BinaryWriter binaryWriter = new(memoryStream);
int index = this.dataContext.Data.Count * MemoryStreamVisualizerObjectSource.RowLength;
if (index >= 1024 * 1024)
{
break; // Let's not retrieve more than 1MB of data
}
binaryWriter.Write(index);
binaryWriter.Flush();
try
{
data = (await this.visualizerTarget.ObjectSource.RequestDataAsync(new ReadOnlySequence<byte>(memoryStream.ToArray()), CancellationToken.None)).Value;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception)
#pragma warning restore CA1031 // Do not catch general exception types
{
// I can get an exception if the debug session is unpaused, so I need to handle it gracefully
break;
}
}
while (data.Length > 0 && this.Read(data));
this.dataContext.LoadingVisibility = Visibility.Hidden;
}
private bool Read(ReadOnlySequence<byte> data)
{
int byteInRowCount = 0;
StringBuilder binaryText = new();
StringBuilder asciiText = new();
SequenceReader<byte> reader = new(data);
if (!reader.TryReadLittleEndian(out long position) || !reader.TryReadLittleEndian(out long length))
{
return false;
}
this.dataContext.Position = position;
this.dataContext.Length = length;
if (reader.UnreadSpan.Length == 0)
{
return false; // We always receive data unless we are at the end of the MemoryStream
}
List<HexEditorRow> rows = new(MemoryStreamVisualizerObjectSource.RowsCountPerRequest);
byte[] tmp = new byte[1];
while (reader.TryRead(out byte b))
{
byteInRowCount++;
if (byteInRowCount > 1)
{
binaryText.Append(' ');
}
binaryText.Append(b.ToString("X2", CultureInfo.InvariantCulture));
tmp[0] = b;
asciiText.Append(char.IsControl((char)b) || b == 0xAD ? '•' : Encoding.Latin1.GetChars(tmp)[0]);
if (byteInRowCount == MemoryStreamVisualizerObjectSource.RowLength)
{
CompleteRow();
}
}
if (byteInRowCount > 0)
{
CompleteRow();
this.dataContext.Data.AddRange(rows);
return false; // We only receive partial rows at the end of the MemoryStream
}
this.dataContext.Data.AddRange(rows);
return true;
void CompleteRow()
{
rows.Add(new HexEditorRow(
index: (this.dataContext.Data.Count + rows.Count) * MemoryStreamVisualizerObjectSource.RowLength,
data: binaryText.ToString(),
ascii: asciiText.ToString()));
byteInRowCount = 0;
binaryText.Clear();
asciiText.Clear();
}
}
}

View File

@@ -0,0 +1,115 @@
<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">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ProgressBar IsIndeterminate="True" VerticalAlignment="Top" Visibility="{Binding LoadingVisibility}" Style="{StaticResource {x:Static styles:VsResourceKeys.ProgressBarStyleKey}}" />
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Label VerticalAlignment="Center" Style="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}">Position: </Label>
<TextBox IsReadOnly="True" Width="75" VerticalAlignment="Center">
<TextBox.Style>
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.TextBoxStyleKey}}">
<Setter Property="Text" Value="{Binding Position, Mode=OneWay}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=HexCheck, FallbackValue=True}" Value="True">
<Setter Property="Text" Value="{Binding HexPosition, Mode=OneWay}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<Label Margin="2,0,0,0" VerticalAlignment="Center" Style="{StaticResource {x:Static styles:VsResourceKeys.ThemedDialogLabelStyleKey}}">Length:</Label>
<TextBox IsReadOnly="True" Width="100" VerticalAlignment="Center">
<TextBox.Style>
<Style TargetType="TextBox" BasedOn="{StaticResource {x:Static styles:VsResourceKeys.TextBoxStyleKey}}">
<Setter Property="Text" Value="{Binding Length, Mode=OneWay}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=HexCheck, FallbackValue=True}" Value="True">
<Setter Property="Text" Value="{Binding HexLength, Mode=OneWay}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox x:Name="HexCheck" Margin="5,0,0,0" VerticalAlignment="Center" Style="{StaticResource {x:Static styles:VsResourceKeys.CheckBoxStyleKey}}">Hex</CheckBox>
</StackPanel>
<DataGrid ItemsSource="{Binding Data}"
EnableRowVirtualization="True"
AutoGenerateColumns="False"
SelectionMode="Extended"
SelectionUnit="Cell"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserResizeRows="False"
CanUserSortColumns="False"
IsReadOnly="True"
Grid.Row="2">
<DataGrid.Resources>
<Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type DataGrid}, ResourceId=DataGridSelectAllButtonStyle}" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Rectangle Fill="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingBackgroundBrushKey}}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</DataGrid.Resources>
<DataGrid.Style>
<Style TargetType="{x:Type DataGrid}">
<Setter Property="Background" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingBackgroundBrushKey}}"/>
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridLineBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
</DataGrid.Style>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingBackgroundBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static colors:ThemedDialogColors.WindowPanelTextBrushKey}}"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.RowHeaderStyle>
<Style TargetType="{x:Type DataGridRowHeader}">
<Setter Property="Background" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingBackgroundBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingTextBrushKey}}"/>
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridLineBrushKey}}"/>
<Setter Property="Content" Value="{Binding Index, Mode=OneWay}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=HexCheck, FallbackValue=True}" Value="True">
<Setter Property="Content" Value="{Binding HexIndex, Mode=OneWay}" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowHeaderStyle>
<DataGrid.ColumnHeaderStyle>
<Style TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingBackgroundBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridHeadingTextBrushKey}}"/>
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static colors:ThemedDialogColors.GridLineBrushKey}}"/>
<Setter Property="Padding" Value="2,0,0,0"/>
</Style>
</DataGrid.ColumnHeaderStyle>
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource {x:Static colors:ThemedDialogColors.ActionButtonStrokeHoverBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static colors:ThemedDialogColors.ActionButtonTextActiveBrushKey}}"/>
<Setter Property="BorderBrush" Value="{DynamicResource {x:Static colors:ThemedDialogColors.ActionButtonStrokeHoverBrushKey}}"/>
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<DataGrid.Columns>
<DataGridTextColumn Header="Hex value" Binding="{Binding Data}" FontFamily="Consolas" />
<DataGridTextColumn Header="Ascii" Binding="{Binding Ascii}" FontFamily="Consolas" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</DataTemplate>

View File

@@ -0,0 +1,23 @@
// <copyright file="Program.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
using System.Text;
// Initialize a MemoryStream to store user input temporarily
using MemoryStream userInputStream = new();
// Convert user input string to byte array
string userInput = "Sample user input data";
byte[] inputData = Encoding.UTF8.GetBytes(userInput);
// Write user input data to the MemoryStream
userInputStream.Write(inputData, 0, inputData.Length);
var buffer = new byte[inputData.Length];
userInputStream.Position = 0;
userInputStream.Read(buffer);
var text = Encoding.UTF8.GetString(buffer, 0, buffer.Length);
// Perform further processing with the stored user input
Thread.Sleep(1000);

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Remove="C:\Users\heilm\source\repos\DebugVisualizer\stylecop.json" />
</ItemGroup>
</Project>