1. Introduction
We have seen lots of good examples of how to create a Debugger Visualizer for Visual Studio. But a common thing among all of them is that all of them are written for Windows Forms. I saw a question on a Microsoft forum asking if it is possible to make a debugger visualizer in WPF, and I decided to do an experiment. After playing with it a little bit, I realized that we can make a debugger visualizer even in WPF. And this opened a whole new door for me, because now I can display data in a much more sophisticated way using this modern technology.
2. Background
Let's take a look at the first step. To make a debugger visualizer, we have to create a class that inherits the DialogDebuggerVisualizer
class. This class is defined using the Microsoft.VisualStudio.DebuggerVisualizers;
namespace, so we have to include its reference too. This namespace defines other classes and interfaces for not only writing the debugger visualizer, but also to debug the visualizer itself. Here is a block diagram to show the classes and interfaces in this namespace:
All of these classes are inherited directly by the Object
class: the grand daddy of .NET framework classes. Here is a class diagram of all the classes defined in the Microsoft.VisualStudio.DebuggerVisualizer
namespace.
There is only one method to overload in the DialogDebuggerVisualizer
class, and here is the signature of that method:
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
The class diagram of this class is very simple. Here is a class diagram of this class. Here, we don’t show the method inherited by the Object
class.
Now, here is a little catch. In a traditional Windows Forms debugger, we call the ShowDialog
method of IDialogVisualizerService
interface that will internally call the Windows Form. The IdialogVisualizerService
interface has only one method ShowDialog
. This method is overloaded and can accept only a Common Dialog, Control, or Form. Here is a class diagram of this interface:
But if we want to make a WPF based visualizer, then we have to create q window object and call it ourselves. In its simplest way, it is something like this:
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
Window win = new Window();
win.Title = "My Visualizer";
win.Width = 400;
win.Height = 300;
win.Background = Brushes.Blue;
win.WindowStartupLocation = WindowStartupLocation.CenterScreen;
win.ShowDialog();
}
Don’t forget to add the references to WindowsBase
, PresentationCore
, PresentationFramework
, and Microsoft.VisualStudio.DebuggerVizualiers
in the project. Here is a screenshot of the Solution Explorer after adding the references to the WPF DLLs and the debugger vizualisers DLL.
This will display the WPF visualizer window with a blue background. Let’s do a little bit more and make a working application. This is just a proof of concept, so we are only making a small application. Our visualizer works only with the Int32
data type. We define it in the form of an attribute when defining the namespace for our visualizer.
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyVisualizerClass), typeof(VisualizerObjectSource),
Target = typeof(System.Int32),Description = "My Visualizer")]
namespace MyVisualizer
{
}
Here, the text set into the Description
property will display as a context menu when this vizualizer will be loaded during the debugging. We select the Int32
data type just for simplicity and because this type is also serializable. If we want to make a visualizer four a custom data type or any other non-serializable data type, then we have to override the GetData
method of the class that implements the IVisualizerObjectProvider
interface.
Now we have two ways to display our data if we are going to use any template for better output. One simple, fast, and proffered way is to write XAML and load that XAML programmatically. The other approach is to make everything programmatically.
3. Defining the Data template with Code
Our target is to display the value of an integer variable in four different formats. We will display the integer in decimal, hex decimal, octal, and binary formats. Let’s first make a small function for the base conversion. I picked this function from CodeProject, originally written by “Balamurali Balaji”, and modified it a little bit to handle negative numbers. Here is a modified version of the base convertor:
private string DecimalToBase(int number, int basenumber)
{
string strRetVal = "";
const int base10 = 10;
char[] cHexa = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
int[] result = new int[32];
int MaxBit = 32;
bool isNegative = false;
if (number < 0)
{
isNegative = true;
number *= -1;
}
for (; number > 0; number /= basenumber)
{
int rem = number % basenumber;
result[--MaxBit] = rem;
}
for (int i = 0; i < result.Length; i++)
{
if ((int)result.GetValue(i) >= base10)
{
strRetVal += cHexa[(int)result.GetValue(i) % base10];
}
else
{
strRetVal += result.GetValue(i);
}
}
strRetVal = strRetVal.TrimStart(new char[] { '0' });
if (isNegative)
{
strRetVal = strRetVal.Insert(0, "-");
}
return strRetVal;
}
We also make a small class for the type and value to store the base name and the converted value in that base. Here is the implementation of that class:
public class TypeValue
{
public TypeValue()
{
}
public TypeValue(String type, String value)
{
Type = type;
Value = value;
}
public String Type
{ get; set; }
public String Value
{ get; set; }
}
Then, we create a list of this class and store all the converted values in it, and then assign it to the listbox.
List<TypeValue> listType = new List<TypeValue>();
Int32 obj = (Int32)objectProvider.GetObject();
listType.Add(new TypeValue("Decimal", obj.ToString()));
listType.Add(new TypeValue("Hex", obj.ToString("X")));
listType.Add(new TypeValue("Octal", DecimalToBase(obj, 8)));
listType.Add(new TypeValue("Binary", DecimalToBase(obj, 2)));
listBox.ItemsSource = listType;
Before that ,we do all the dirty work for data binding and defining the data template and the visual tree of the data template.
We also add one more static method in the class just for debugging purposes. It is not necessary to add this method in this class, but it will help us debug the vizualier.
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyVisualizerClass));
host.ShowVisualizer();
}
Here is the complete code for this visualizer:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.DebuggerVisualizers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Data;
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyVisualizerClass), typeof(VisualizerObjectSource),
Target = typeof(System.Int32),Description = "My Visualizer")]
namespace MyVisualizer
{
public class MyVisualizerClass : DialogDebuggerVisualizer
{
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
Window win = new Window();
win.Title = "My Visualizer";
win.Width = 400;
win.Height = 300;
win.Background = Brushes.Blue;
win.WindowStartupLocation = WindowStartupLocation.CenterScreen;
ListBox listBox = new ListBox();
listBox.Margin = new Thickness(10);
listBox.HorizontalContentAlignment = HorizontalAlignment.Stretch;
DataTemplate dt = new DataTemplate();
Binding bindingType = new Binding();
bindingType.Path = new PropertyPath("Type");
Binding bindnigValue = new Binding();
bindnigValue.Path = new PropertyPath("Value");
FrameworkElementFactory textType =
new FrameworkElementFactory(typeof(TextBlock));
textType.SetBinding(TextBlock.TextProperty, bindingType);
textType.SetValue(Control.ForegroundProperty, Brushes.Blue);
textType.SetValue(Control.FontWeightProperty, FontWeights.Bold);
FrameworkElementFactory textValue =
new FrameworkElementFactory(typeof(TextBlock));
textValue.SetBinding(TextBlock.TextProperty, bindnigValue);
textValue.SetValue(Control.ForegroundProperty, Brushes.Black);
FrameworkElementFactory stack =
new FrameworkElementFactory(typeof(StackPanel));
stack.SetValue(Control.MarginProperty, new Thickness(5));
stack.AppendChild(textType);
stack.AppendChild(textValue);
FrameworkElementFactory border = new FrameworkElementFactory(typeof(Border));
border.SetValue(Control.BackgroundProperty, Brushes.LightYellow);
border.SetValue(Control.BorderBrushProperty, Brushes.Brown);
border.SetValue(Control.BorderThicknessProperty, new Thickness(3));
border.SetValue(Control.MarginProperty, new Thickness(5));
border.AppendChild(stack);
dt.VisualTree = border;
listBox.ItemTemplate = dt;
List<TypeValue> listType = new List<TypeValue>();
Int32 obj = (Int32)objectProvider.GetObject();
listType.Add(new TypeValue("Decimal", obj.ToString()));
listType.Add(new TypeValue("Hex", obj.ToString("X")));
listType.Add(new TypeValue("Octal", DecimalToBase(obj, 8)));
listType.Add(new TypeValue("Binary", DecimalToBase(obj, 2)));
listBox.ItemsSource = listType;
win.Content = listBox;
win.ShowDialog();
}
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyVisualizerClass));
host.ShowVisualizer();
}
private string DecimalToBase(int number, int basenumber)
{
string strRetVal = "";
const int base10 = 10;
char[] cHexa = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
int[] result = new int[32];
int MaxBit = 32;
bool isNegative = false;
if (number < 0)
{
isNegative = true;
number *= -1;
}
for (; number > 0; number /= basenumber)
{
int rem = number % basenumber;
result[--MaxBit] = rem;
}
for (int i = 0; i < result.Length; i++)
{
if ((int)result.GetValue(i) >= base10)
{
strRetVal += cHexa[(int)result.GetValue(i) % base10];
}
else
{
strRetVal += result.GetValue(i);
}
}
strRetVal = strRetVal.TrimStart(new char[] { '0' });
if (isNegative)
{
strRetVal = strRetVal.Insert(0, "-");
}
return strRetVal;
}
}
public class TypeValue
{
public TypeValue()
{
}
public TypeValue(String type, String value)
{
Type = type;
Value = value;
}
public String Type
{ get; set; }
public String Value
{ get; set; }
}
}
Now, let’s make a small program to test this visualizer. In this small program, we just create an integer variable and see its value in our vizualizer:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyVisualizer;
namespace MyVisualizerClient
{
class Program
{
[STAThread]
static void Main(string[] args)
{
int iValue = -100;
MyVisualizerClass.TestShowVisualizer(iValue);
Console.WriteLine(iValue);
}
}
}
When we copy the visualizer DLL at the specified location (in my computer, it is C:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers), then for all integer variables, we can see the “My Visualizer” option in the context menu during debugging.
It is also available at the watch window, as shown in this screenshot:
And when we click on it, we can see our visualizer window with the listbox in it to display the value of the integer in hex, octal, and binary formats.
4. Defining the Data Template with XAML
Although we can define the data template in code completely, it is not recommended. In addition, we have to write lots of code. Now, let’s take a look at the recommended approach. This time, we are going to make a XAML file and load it at run time. This approach will make our work easier, and we can use the full power of XAML, and also that of WPF very easily.
Let’s first make a XAML file. Notice that this code is very small as compared to define everything in C#. Here is our XAML file:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My Vsualizer" Height="300" Width="400" Background="Blue"
WindowStartupLocation="CenterScreen">
<ListBox Name="listBox" Margin="10" HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="LightYellow"
BorderBrush="Brown" BorderThickness="5">
<StackPanel Margin="5">
<TextBlock Foreground="Black"
FontWeight="Bold" Text="{Binding Path=Type}"/>
<TextBlock Foreground="Black" Text="{Binding Path=Value}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>
Now, we are going to load the XAML file using the XamlReader
class. Here is the code to load the XAML file at run time.
Window win = null;
FileStream fs = new FileStream("VisualWindow.xaml", FileMode.Open, FileAccess.Read);
win = (Window)XamlReader.Load(fs);
fs.Close();
If we want to access any control defined in the XAML file, then we can use the FindName
function. Here is the code to access the ListBox
defined in the XAML file:
ListBox listBox = win.FindName("listBox") as ListBox;
We have to include two more namespaces to read the XAML file. These namespaces are System.IO
and System.Windows.Markup
. Here is the complete C# code to load the XAML file at runtime and display the integer value in different formats:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.DebuggerVisualizers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.IO;
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyVisualizerClass), typeof(VisualizerObjectSource),
Target = typeof(System.Int32),Description = "My Visualizer")]
namespace MyVisualizer
{
public class MyVisualizerClass : DialogDebuggerVisualizer
{
private Int32 obj;
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
obj = (Int32)objectProvider.GetObject();
List<TypeValue> listType = new List<TypeValue>();
listType.Add(new TypeValue("Decimal", obj.ToString()));
listType.Add(new TypeValue("Hex", obj.ToString("X")));
listType.Add(new TypeValue("Octal", DecimalToBase(obj, 8)));
listType.Add(new TypeValue("Binary", DecimalToBase(obj, 2)));
Window win = null;
FileStream fs = new FileStream("VisualWindow.xaml",
FileMode.Open, FileAccess.Read);
win = (Window)XamlReader.Load(fs);
fs.Close();
ListBox listBox = win.FindName("listBox") as ListBox;
listBox.ItemsSource = listType;
win.ShowDialog();
}
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyVisualizerClass));
host.ShowVisualizer();
}
private string DecimalToBase(int number, int basenumber)
{
string strRetVal = "";
const int base10 = 10;
char[] cHexa = new char[] { 'A', 'B', 'C', 'D', 'E', 'F' };
int[] result = new int[32];
int MaxBit = 32;
bool isNegative = false;
if (number < 0)
{
isNegative = true;
number *= -1;
}
for (; number > 0; number /= basenumber)
{
int rem = number % basenumber;
result[--MaxBit] = rem;
}
for (int i = 0; i < result.Length; i++)
{
if ((int)result.GetValue(i) >= base10)
{
strRetVal += cHexa[(int)result.GetValue(i) % base10];
}
else
{
strRetVal += result.GetValue(i);
}
}
strRetVal = strRetVal.TrimStart(new char[] { '0' });
if (isNegative)
{
strRetVal = strRetVal.Insert(0, "-");
}
return strRetVal;
}
}
public class TypeValue
{
public TypeValue()
{
}
public TypeValue(String type, String value)
{
Type = type;
Value = value;
}
public String Type
{ get; set; }
public String Value
{ get; set; }
}
}
We have to copy the VisualWindow.xaml file to the same folder where the DLL resides, i.e., in the debugger visualer folder. Now, if the user clicks on the MyVisualizer menu items either at the code window or at the watch window, then a WPF window will pop up and its output would be something like this:
5. Summary
We created a simple Debugger visualizer in WPF with the integer data type. But this is a read only visualizer and supports only one data type. In the second part of this article, we will extend this concept to add support for multiple data types. In addition, we will also add support for editing data.