Introduction
DebuggerVisualizers allow you to customize the way objects are displayed while debugging in Visual Studio. This is useful for a great number of applications. Some examples might be:
- Building simplified models of complex objects.
- Rendering objects which are composed of more than just basic types into a human viewable form.
- Presenting debug data in a more organized, and easily accessible, way.
In our DotImage Toolkit, we provide an AtalaImage object in which the image data itself is stored. Using a DebuggerVisualizer allows us to inspect that image visually, on the fly, while debugging. As you might expect, this is much easier than injecting statements in the code to write out intermediary images or trying to interpret the raw contents of the image memory.
The process for building Visual Studio Debugger Visualizers is extremely straightforward for most objects. However, for objects which do not implement ISerializable, serialize too slowly or do not contain all of the information you may wish to see within their serialized form, it is necessary to package a custom serializer along with your Debugger Visualizer.
Factors to Consider
- Visualizers with custom serializers need to be able to reference the assemblies for the object which is being custom serialized.
- Visualizers with custom serializers depend on the visualizer, serializer and the current project to all be referencing the same version assemblies.
- Visualizers will time out if the serialization or deserialization takes too long.
- Visualizers only work with Visual Studio 2005 or newer.
Code
When building a Visualizer with custom serialization, there are four main components in play: the Custom Serializer, the Transport Object, the Visualizer/Deserializer and the Data Viewer.
The Custom Serializer (AtalaImageSerializer.cs)
A custom serializer lets you define serialization for an object in any way you like, even if it is already serializable. It is instantiated and run on the debugee side and so has access to the entire debug environment. Implementing a Custom Serializer is as simple as overriding the GetData method in VisualizerObjectSource.
By default, we serialize AtalaImages to PNG format. PNG is great for saving space, but encoding and decoding is much too slow and was causing our DebuggerVisualizer to timeout with large images. To remedy this, we built a custom serializer which outputs to BMP instead. Using BMP has the added benefit of being compatible with System.Windows.Forms.PictureBox, allowing us to not have to load our assemblies on the Debugger side.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Atalasoft.Imaging;
namespace AtalaImageDebuggerVisualizer
{
class AtalaImageSerializer :
Microsoft.VisualStudio.DebuggerVisualizers.VisualizerObjectSource
{
public override void GetData(object inObject, Stream outStream)
{
if (inObject != null && inObject is AtalaImage)
{
AtalaImage atalaImage = inObject as AtalaImage;
Bitmap bmp = atalaImage.ToBitmap();
AtalaImageTransporter transporter =
new AtalaImageTransporter(bmp, atalaImage.PixelFormat.ToString());
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(outStream, transporter);
}
}
}
}
inObject
is the object you are are trying to visualize and outStream
is your data connection to the Visualizer. outStream
is just a normal stream, and so you can dump anything you want into it. In this case, I used a transport object in order to wrap only the information I need for my Visualizer for convienance and code readability.
Visual Studio will timeout if you take too long with your serialization (or deserialization), so the speed of serialization is key. If you are having timeout problems, you may want to check out the following article on The Code Project about speeding up Serialization: Optimizing Serialization in .NET by SimmoTech.
The Transport Object (AtalaImageTransporter.cs)
The transport object’s job is to wrap up all of the information I want to access in my Visualizer into a nice, neat package. Having all of the information in an object also makes Serialization and Deserialization much easier.
using System;
using System.Drawing;
using System.Runtime.Serialization;
namespace AtalaImageDebuggerVisualizer
{
[Serializable]
public class AtalaImageTransporter
{
public AtalaImageTransporter(Image pic, string pf)
{
Picture = pic;
PixelFormat = pf;
}
Image _picture;
public Image Picture
{
get { return _picture; }
set { _picture = value; }
}
string _pixelformat;
public string PixelFormat
{
get { return _pixelformat; }
set { _pixelformat = value; }
}
}
}
For my purposes, the default serialization methods for string and image worked great. If you want more fin-grained control over the serialization of your data, you should make your transport object implement ISerializable.
The Visualizer/Deserializer (AtalaImageVisualizer.cs)
The Visualizer receives your serialized data and is responsible for deserializing it and presenting it to the user. It is instantiated and run on the debugger side, and so only has access to the data provided in objectProvider
. Like the custom serializer, all that is needed to implement a DebuggerVisualizer is to inherit from a super class and override one method. In this case, the class is DialogDebuggerVisualizer and the method is Show.
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Atalasoft.Imaging;
using Microsoft.VisualStudio.DebuggerVisualizers;
namespace AtalaImageDebuggerVisualizer
{
public class AtalaImageVisualizer : Microsoft.VisualStudio.
DebuggerVisualizers.DialogDebuggerVisualizer
{
protected override void Show(Microsoft.VisualStudio.
DebuggerVisualizers.IDialogVisualizerService windowService,
Microsoft.VisualStudio.DebuggerVisualizers.
IVisualizerObjectProvider objectProvider)
{
Stream stm = objectProvider.GetData();
if (stm.Length != 0)
{
BinaryFormatter bf = new BinaryFormatter();
AtalaImageTransporter imageData =
bf.Deserialize(stm) as AtalaImageTransporter;
if (imageData != null)
{
AtalaImageViewer view = new AtalaImageViewer(imageData);
view.ShowDialog();
}
}
}
}
}
IVisualizerObjectProvider
provides a method named GetObject
which returns an object directly. However, depending on how you serialized your data, direct deserialization to an object may not work and so deserializing from a stream is safer.
The Data Viewer (AtalaImageViewer.cs)
The Data Viewer is just a simple form used to display the Visualized data to the user. Using a form is not strictly necessary, but it is standard practice.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace AtalaImageDebuggerVisualizer
{
public partial class AtalaImageViewer : Form
{
public AtalaImageViewer()
{
InitializeComponent();
}
public AtalaImageViewer( AtalaImageTransporter imageData )
{
InitializeComponent();
PicViewer.Image = imageData.Picture;
PixelFormatLabel.Text = imageData.PixelFormat;
PicViewer.SizeMode = PictureBoxSizeMode.Zoom;
this.Width = imageData.Picture.Width / 2;
this.Height = imageData.Picture.Height / 2;
}
}
}
The Assembly Description (AssemblyInfo.cs)
The DebuggerVisualizerAttribute describes how the visualizer is used to Visual Studio:
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(AtalaImageDebuggerVisualizer.AtalaImageVisualizer),
typeof(AtalaImageDebuggerVisualizer.AtalaImageSerializer),
Target = typeof(Atalasoft.Imaging.AtalaImage),
Description = "Atalasoft Image Visualizer" )]
The Format is:
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof( Your Visualizer ),
typeof( Your Custom Serializer ),
Target = typeof( Your Visualized Object ),
Description = "Description Here" )]
Replacing the Visualized Object
If you want to change your object inside your visualizer and inject it back into the program that is being debugged, you need to do two additional things:
- The IVisualizerObjectProvider inside the visualizer provides the
ReplaceStream
method that is used for sending data back. You need to serialize and send your Transport Object back using this method.
- Inside your custom serializer you need to override VisualizerObjectSource’s CreateReplacementObject method. You need to deserialize your transport object, build a replacement for the visualized object and assign that to the target parameter.
Conclusion
This article by no means covers all of the intimate details of DebuggerVisualizers or custom serialization. If you are building one, I encourage you to visit the MSDN documentation which will provide for a much deeper level of understanding.
Atalasoft, Inc., is a leading provider of .NET development toolkits for ISVs, integrators, and enterprises with the need to add Enterprise Content Management Imaging (ECMi) to their solutions. Atalasoft’s products are used in many industries including Financial Services, Legal, Healthcare, and Manufacturing with requirements for distributed capture, zero-footprint web viewing, and document markup.