This posting is part of a series detailing how to write a custom Debugger Visualizer for Visual Studio.
Posts in the series:
- Writing A (ReadOnly) Custom Debugger Visualizer for Visual Studio
- Writing A Custom Debugger Visualizer for Non-Serializable Types or More Complex Objects
- Writing A Custom Debugger Visualizer using WPF for the UI
- Modifying The Live Object in Your Custom Debugger Visualizer for Visual Studio
- Installation, Limitations, and Security Considerations for Your Custom Debugger Visualizer for Visual Studio
I've also given a talk on this topic: Putting the Visual into the Visual Studio Debugger.
You can find code samples to go along with this series in my DebugVisualizer
GitHub repository.
This post builds upon the first post in the series Writing A (ReadOnly) Custom Debugger Visualizer for Visual Studio. Please at least skim through that post to understand the topics in this one.
Non-Serializable Types and/or More Complex Objects
In the real world, our data types aren't super-simple, and it's likely you'll want to visualize an object (or object graph) that has non-serializable parts. The first visualizer I wrote actually falls into this case. I wanted to visualize WebException
, which is actually Serializable, but the main thing I wanted access to (which I can't get in the built-in debugger tooling) is the actual HTML response returned from the website.
Consider this scenario:
You have an ASP.NET WebAPI site and an application/website that calls that API from C# code. The WebAPI throws a 500 error. When this happens (assuming you've turned on the appropriate configuration options), the returned HTML includes stacktraces and other helpful information. But since it's a non-successful response code, the call results in a
WebException
being thrown in the calling code. I often want to look at the raw returned response to get additional details, which is exposed as aStream
from theWebException.GetResponseStream()
method -- which isn't included in any serialized instance of the exception.
So, my visualizer can't just pass the WebException
object through the VisualizerObjectSource
and get the raw response content. Instead, I need to extract the information I want passed over to my visualizer and return my own class with the information. Think of this as the ViewModel of your visualizer -- it exposes the members of the Model (the exception) to your View (the visualiser UI) in an easily consumable way.
To do this, I need to get a little more advanced with my components:
My ViewModel classes
I've split the ViewModel into two classes: One for the WebException
and one for the WebResponse
object it contains. I did this in order to make the WebResponse
viewmodel reusable in other visualizers.
[Serializable]
public class WebExceptionViewModel
{
private readonly ResponseViewModel _response;
/// <summary>
/// The original Exception
/// </summary>
private readonly System.Net.WebException _exception;
public WebExceptionViewModel(System.Net.WebException ex)
{
_exception = ex;
if (ex.Response != null)
{
_response = new ResponseViewModel(ex.Response);
}
}
public bool HasResponse => _response != null;
public ResponseViewModel Response => _response;
public string Message => _exception.Message;
public string StackTrace => _exception.ToString();
public string Status => _exception.Status.ToString();
}
[Serializable]
public class ResponseViewModel
{
public long ContentLength { get; }
public string ContentType { get; }
public string ResponseUri { get; }
public string RawResponse { get; }
public ResponseViewModel(System.Net.WebResponse response)
{
ContentLength = response.ContentLength;
ContentType = response.ContentType;
ResponseUri = response.ResponseUri.ToString();
var stream = response.GetResponseStream();
if (stream != null)
{
// save position before reading
long position = stream.Position;
stream.Seek(0, SeekOrigin.Begin);
using (var readStream = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
{
RawResponse = readStream.ReadToEnd();
}
// reset seek position so we don't have side effects on the app
stream.Seek(position, SeekOrigin.Begin);
}
}
}
The key piece is the part of the WebResponse
constructor that reads the response Stream
and stores the results as a string property. Now, that data will be included in the serialized object that is passed back to the visualizer UI.
IMPORTANT NOTE: Did you notice how I store off the current stream position and restore it after I'm done -- and that I don't close the stream? That's because I don't want my visualizer to have side effects on the application being debugged. If the application's try/catch block is trying to read that stream too, and I happened to pause the debugger and visualize the object part-way through that, I need to make sure the app can continue running after I close my visualizer and unpause the debugging session. Keep this in mind when writing your own visualizers -- if you modify the object in any way, then when the app starts running again, it will be using the modified version of the object. (We'll do this on purpose in my next post when we make a read-write visualizer.)
Visualizer Object Source
Now that I've got a ViewModel to wrap around my primary object, I can no longer use the built-in VisualizerObjectSource
-- I need to have a customized VisualizerObjectSource
class that will return the view model instead of the original object being visualized.
Recall that VisualizerObjectSource
has a GetData()
method that's called to fetch the serialized object to display. So I've written a class that extends VisualizerObjectSource
and overridden the GetData()
implementation to use the view model class instead of the exception class. I countinue to use the base implementation to do the actual serialization, but I don't have to -- I could write directly to the outgoingData Stream
and return.
public class WebExceptionVisualObjectSource : VisualizerObjectSource
{
public override void GetData(object target, Stream outgoingData)
{
var originalObj = target as System.Net.WebException;
var viewModel = new WebExceptionViewModel(originalObj);
base.GetData(viewModel, outgoingData);
}
}
The UI
My UI is still just a simple WinForm with a few TextBox controls on it:
The Visualizer
My Visualizer class still calls objectProvider.GetObject()
, but now casts the result as my WebExceptionViewModel
class. Then I again create my WinForms UI object, set the value and display it.
public class SimpleWebExceptionVisualizer : DialogDebuggerVisualizer
{
protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
{
if (windowService == null)
throw new ArgumentNullException(nameof(windowService));
if (objectProvider == null)
throw new ArgumentNullException(nameof(objectProvider));
// Get the object to display a visualizer for.
// Cast the result of objectProvider.GetObject()
// to the type of the object being visualized.
var data = objectProvider.GetObject() as WebExceptionViewModel;
// Display your view of the object.
using (var displayForm = new SimpleWebExceptionWinForm())
{
if (data == null)
{
displayForm.txtMessage.Text = "<null>";
}
else
{
displayForm.Text = objectProvider.GetType().FullName; // TitleBar
displayForm.txtMessage.Text = data.Message;
displayForm.txtStackTrace.Text = data.StackTrace;
displayForm.txtResponse.Text = data.HasResponse ? data.Response.RawResponse : "<null>";
}
windowService.ShowDialog(displayForm);
}
}
}
Registering The Visualizer
I'll use the DebuggerVisualizer
attribute again to register my new debugger, but this time I'm not using the built-in VizualizerObjectSource
, so I need to tell the Debugger which class to use as the 2nd parameter to the attribute:
[assembly: DebuggerVisualizer(
typeof(TotallyRealApp.DebuggerVisualizer.SimpleWebExceptionVisualizer), //Your VS-side type
typeof(WebExceptionVisualObjectSource), //the incoming type's provider -- default is ok if Type is serializable
Target = typeof(WebException), //the type you want to visualize
Description = "Simple WebException Visualizer")] //name shown in visualizer picker
And now I can see the full HTML of the result:
More Advanced Topic: Using WPF for the UI
Ok, so far my UIs have been pretty basic, but as I start to visualize more complex object types, I'm going to need more complex UIs. And personally, I find that hard to do with WinForms. Plus, I'd rather use MVVM-style UI binding instead of the code-behind style typical with WinForms. So, I want to use WPF for my UI instead of WinForms.
Thankfully, this is super easy to do!
I'll use the same ViewModel and VizualizerObjectSource as I did for my WinForms visualizer, but on the Visual Studio side, I'll have a different UI and a different Visualizer class:
The UI
Obviously, the UI will be WPF-based now. I'll use DataContext binding in my XAML to bind directly against my ViewModel class.
<UserControl x:Class="TotallyRealApp.DebuggerVisualizer.WebException.WebExceptionVisualizerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TotallyRealApp.DebuggerVisualizer.WebException"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="500"
MinWidth="500" MinHeight="500" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
d:DataContext="{d:DesignInstance Type=local:WebExceptionViewModel, IsDesignTimeCreatable=False}"
Padding="5" Margin="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="500"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0 5" Grid.Row="0">
<Label Margin="0 0 5 0" Padding="0" FontWeight="Bold">Message:</Label>
<TextBlock TextWrapping="Wrap" Text="{Binding Path=Message, Mode=OneWay}" Height="auto" />
</StackPanel>
<Label Grid.Row="1" Margin="0 5" Padding="0" FontWeight="Bold">Stack Trace:</Label>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto">
<TextBlock Width="Auto" TextWrapping="Wrap" Text="{Binding Path=StackTrace, Mode=OneWay}"></TextBlock>
</ScrollViewer>
<Label Margin="0 0 5 0" Padding="0 5" FontWeight="Bold" Grid.Row="3">Raw Response:</Label>
<ScrollViewer HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto" Grid.Row="4">
<TextBlock TextWrapping="NoWrap" Text="{Binding Path=Response.RawResponse, Mode=OneWay}" Width="Auto"></TextBlock>
</ScrollViewer>
</Grid>
</UserControl>
The Visualizer
The other change that's needed to use WPF is to the Visualizer. Instead of using the passed-in IDialogVisualizerService
to show a dialog, we'll create an instance of a WPF Window, put our WPF UserControl
on it, and show that window.
public class WpfWebExceptionVisualizer : DialogDebuggerVisualizer
{
protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
{
if (objectProvider == null)
throw new ArgumentNullException(nameof(objectProvider));
// Get the object to display a visualizer for.
// Cast the result of objectProvider.GetObject()
// to the type of the object being visualized.
var data = objectProvider.GetObject() as WebExceptionViewModel;
// Display your view of the object.
var vizControl = new WebExceptionVisualizerControl { DataContext = data };
// set the attributes of WPF window
var win = new Window
{
Title = "WPF Web Exception Visualizer",
MinWidth = 500,
MinHeight = 500,
Padding = new Thickness(5),
Margin = new Thickness(5),
WindowStartupLocation = WindowStartupLocation.CenterScreen,
Content = vizControl,
VerticalContentAlignment = VerticalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Stretch
};
win.ShowDialog();
}
}
Make sure to take a look at the other posts in my Debugger Visualizer series to improve upon your visualizer.