Writing a Debugger Visualizer in WPF: Part 2






4.45/5 (9 votes)
This article describes writing a debugger visualizer in WPF for multiple data types.
1. Introduction
In the first article, we discussed how to create a basic debugger visualizer in WPF. Our visualizer was read only and supported only one data type. Now, let’s extend this concept and try to improve it.
2. Editable visualizer
In the previous article, we created a visualizer using a XAML file. Now, let’s move forward and improve the functionality of our visualizer. Our previous visualizer was a read only visualizer, i.e., we could only view the data in it. Now we are going to make a visualizer that is interactive, i.e., it has a feature to change the value of a variable.
The basic concept behind this is to use the ReplaceObject
function of the IVisualizerObjectProvider
interface. Here is a class diagram of this interface.
We add a TextBox
in our WPF window to enter data. Here is a piece of code of that new TextBox
:
<Border Margin="10" Background="AliceBlue" BorderBrush="Navy"
BorderThickness="5" CornerRadius="5">
<StackPanel>
<TextBlock Margin="5">Enter new Value</TextBlock>
<TextBox Name="txtValue" Margin="5"/>
</StackPanel>
</Border>
The name of our new TextBox
is txtValue
. We use the same FindWindow
technique which we used earlier, to get the value of this TextBox
, and then call ReplaceObject
. After that, we close the window.
TextBox text = win.FindName("txtValue") as TextBox;
Int32 newValue = Convert.ToInt32(text.Text);
if (objProvider.IsObjectReplaceable)
{
objProvider.ReplaceObject(newValue);
}
win.Close();
Here is a complete XAML code of this program:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My Vsualizer" Height="400" Width="400" Background="Wheat"
WindowStartupLocation="CenterScreen">
<StackPanel>
<Border Margin="10" Background="AliceBlue"
BorderBrush="Navy" BorderThickness="5" CornerRadius="5">
<StackPanel>
<TextBlock Margin="5">Enter new Value</TextBlock>
<TextBox Name="txtValue" Margin="5"/>
</StackPanel>
</Border>
<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>
<Button Name="btnOK" Margin="10" Width="75">OK</Button>
</StackPanel>
</Window>
Here is the complete C# code for this project:
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;
private Window win;
private IVisualizerObjectProvider objProvider;
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
objProvider = objectProvider;
obj = (Int32)objProvider.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)));
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;
Button buttonOK = win.FindName("btnOK") as Button;
buttonOK.Click += new RoutedEventHandler(buttonOK_Click);
win.ShowDialog();
}
void buttonOK_Click(object sender, RoutedEventArgs e)
{
TextBox text = win.FindName("txtValue") as TextBox;
Int32 newValue = Convert.ToInt32(text.Text);
if (objProvider.IsObjectReplaceable)
{
objProvider.ReplaceObject(newValue);
}
win.Close();
}
// This function is only for debugging purpose
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyVisualizerClass));
host.ShowVisualizer();
}
// Orignally written by Balamurali Balaji
// Changed little bit to handle the negative sig
// http://www.codeproject.com/KB/cs/balamurali_balaji.aspx
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 when we click on the MyVisualizer, either a Watch window or a context menu appears. If we click on the integer variable name, then we will see the following window:
3. Support for more than one data type
Till now, our visualizer was limited to only one data type, the integer data type. But any good visualizer supports multiple data types. Let’s extend our visualizer a little bit more and handle more than one data type with it. In this case, we are going to add string data type support in our visualizer. This time, we are going to display a string in upper case, lower case, its length, and in base 64 encoded format.
We define the attributes in our namespace which defines the supported data types by the visualizer. If we want to make our visualizer support more than one data type, then we have to define this attribute more than once with different data types.
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyInt32Class), typeof(VisualizerObjectSource),
Target = typeof(System.Int32),Description = "My Visualizer")]
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyStringClass), typeof(VisualizerObjectSource),
Target = typeof(System.String), Description = "My Visualizer")]
namespace MyVisualizer
{
}
Our visualizer now supports two data types: integer and string. For simplicity, we create different classes to handle string and integer data types. Our string class is almost similar to the integer class, except it displays information in different formats. Just for simplicity, we use the same window class for both integer and string types, but this is not a requirement.
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
List<TypeValue> listType = new List<TypeValue>();
listType.Add(new TypeValue("String", obj));
listType.Add(new TypeValue("Upper", obj.ToUpper()));
listType.Add(new TypeValue("Lower", obj.ToLower()));
listType.Add(new TypeValue("Length", obj.Length.ToString()));
listType.Add(new TypeValue("Base 64 Encoding",
Convert.ToBase64String(encoding.GetBytes(obj))));
And here is the complete C# code for our project:
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.MyInt32Class), typeof(VisualizerObjectSource),
Target = typeof(System.Int32),Description = "My Visualizer")]
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyStringClass), typeof(VisualizerObjectSource),
Target = typeof(System.String), Description = "My Visualizer")]
namespace MyVisualizer
{
public class MyStringClass : DialogDebuggerVisualizer
{
private Window win;
private String obj;
private IVisualizerObjectProvider objProvider;
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
objProvider = objectProvider;
obj = (String)objProvider.GetObject();
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
List<TypeValue> listType = new List<TypeValue>();
listType.Add(new TypeValue("String", obj));
listType.Add(new TypeValue("Upper", obj.ToUpper()));
listType.Add(new TypeValue("Lower", obj.ToLower()));
listType.Add(new TypeValue("Length", obj.Length.ToString()));
listType.Add(new TypeValue("Base 64 Encoding",
Convert.ToBase64String(encoding.GetBytes(obj))));
FileStream fs = new FileStream("VisualizerWindow.xaml",
FileMode.Open, FileAccess.Read);
win = (Window)XamlReader.Load(fs);
win.Height = 450;
fs.Close();
ListBox listBox = win.FindName("listBox") as ListBox;
listBox.ItemsSource = listType;
Button buttonOK = win.FindName("btnOK") as Button;
buttonOK.Click += new RoutedEventHandler(buttonOK_Click);
win.ShowDialog();
}
void buttonOK_Click(object sender, RoutedEventArgs e)
{
TextBox text = win.FindName("txtValue") as TextBox;
String newValue = text.Text;
if (objProvider.IsObjectReplaceable)
{
objProvider.ReplaceObject(newValue);
}
win.Close();
}
// This function is only for debugging purpose
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyStringClass));
host.ShowVisualizer();
}
}
public class MyInt32Class : DialogDebuggerVisualizer
{
private Int32 obj;
private Window win;
private IVisualizerObjectProvider objProvider;
protected override void Show(IDialogVisualizerService windowService,
IVisualizerObjectProvider objectProvider)
{
objProvider = objectProvider;
obj = (Int32)objProvider.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)));
FileStream fs = new FileStream("VisualizerWindow.xaml",
FileMode.Open, FileAccess.Read);
win = (Window)XamlReader.Load(fs);
fs.Close();
ListBox listBox = win.FindName("listBox") as ListBox;
listBox.ItemsSource = listType;
Button buttonOK = win.FindName("btnOK") as Button;
buttonOK.Click += new RoutedEventHandler(buttonOK_Click);
win.ShowDialog();
}
void buttonOK_Click(object sender, RoutedEventArgs e)
{
TextBox text = win.FindName("txtValue") as TextBox;
Int32 newValue = Convert.ToInt32(text.Text);
if (objProvider.IsObjectReplaceable)
{
objProvider.ReplaceObject(newValue);
}
win.Close();
}
// This function is only for debugging purpose
public static void TestShowVisualizer(object obj)
{
VisualizerDevelopmentHost host =
new VisualizerDevelopmentHost(obj, typeof(MyInt32Class));
host.ShowVisualizer();
}
// Orignally written by Balamurali Balaji
// Changed little bit to handle the negative sign
// http://www.codeproject.com/KB/cs/balamurali_balaji.aspx
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 if we use this visualizer for the string data type, then its output will be something like this:
4. Non-serializerable class
Till now, we studied how to create a debugger visualizer to support multiple data types in WPF. But we were using serializable data types for this. What will be the situation if we want to create a debugger visualizer for a non-serializable data type, such as Color
, Brush
, etc.? Now, we are going to extend our visualizer to support non-serializable data types like Color
.
The first thing we have to do is to add the attribute in our namespace for the Color
type. For simplicity, we create another class name MyColorClass
to handle the Color
type. Here is the declaration of the namespace:
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyInt32Class), typeof(VisualizerObjectSource),
Target = typeof(Int32),Description = "My Visualizer")]
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyStringClass), typeof(VisualizerObjectSource),
Target = typeof(String), Description = "My Visualizer")]
[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(MyVisualizer.MyColorClass), typeof(MyVisualizer.ColorSource),
Target = typeof(Color), Description = "My Visualizer")]
namespace MyVisualizer
{
}
Now our visualizer supports three data types: integer, string, and color.
In addition, there is one more difference. Instead of using VisualizerObjectSource
, we are going to use the ColorSource
class which is in fact inherited by the VisualizerObjectSource
class.
public class ColorSource : VisualizerObjectSource
{
}
Here is a class diagram of the VisualizerObjectSource
class:
For every non-serializable class, we have to do the serialization ourselves, and that’s exactly the reason to inherit the class from the VisualizerObjectSource
class. Here, we override the GetData
function and perform the serialization of the object.
public class ColorSource : VisualizerObjectSource
{
public override void GetData(object target, Stream outgoingData)
{
if (target != null && target is Color)
{
Color color = (Color)target;
MyColor obj = new MyColor(color);
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(outgoingData, obj);
}
}
}
We have to include a namespace for the BinaryFormatter
.
using System.Runtime.Serialization.Formatters.Binary;
But the Color
class is non-serializable and whenever we are going to perform serialization on it, we will get a SerializationException
. The solution is to create a wrapper class of that data type, implement the ISerializable
interface, and the define the Serializable
attribute for our class. This interface has only one method to overload, named GetObjectData
. Here is a class diagram of the ISerializable
interface:
Here is a wrapper class of Color
which implements this interface:
[Serializable]
public class MyColor : ISerializable
{
}
We can’t create an object of this class, because it hasn’t overridden the GetObjectData
method. In addition, this class doesn’t do anything with the Color
type. Here is a complete implementation of this class:
[Serializable]
public class MyColor : ISerializable
{
private Color _color;
public MyColor()
{
}
public MyColor(Color color)
{
this.color = color;
}
public Color color
{
get { return _color; }
set { _color = value; }
}
protected MyColor(SerializationInfo info, StreamingContext context)
{
_color.A = info.GetByte("A");
_color.R = info.GetByte("R");
_color.G = info.GetByte("G");
_color.B = info.GetByte("B");
}
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter = true)]
public virtual void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("A", color.A);
info.AddValue("R", color.R);
info.AddValue("G", color.G);
info.AddValue("B", color.B);
}
public override string ToString()
{
return String.Format("({0}, {1}, {2}, {3})",
color.A.ToString(),
color.R.ToString(),
color.G.ToString(),
color.B.ToString());
}
}
We also override the ToString
method to display the different components of the color.
Once we are done with it, then the rest of the stuff is quite easy. We created different XAML files to display the name of the color and display a rounded border in that color. Here is the complete XAML file for this project for the Color
type:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My Vsualizer" Height="400" Width="400" Background="Wheat"
WindowStartupLocation="CenterScreen">
<StackPanel>
<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>
<Border Name="colorBorder" Margin="10" Height="250"
BorderBrush="Black" BorderThickness="1" CornerRadius="10" />
</StackPanel>
</Window>
And, here is the XAML file to display the integer and string types:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="My Vsualizer" Height="400" Width="400" Background="Wheat"
WindowStartupLocation="CenterScreen">
<StackPanel>
<Border Margin="10" Background="AliceBlue"
BorderBrush="Navy" BorderThickness="5" CornerRadius="5">
<StackPanel>
<TextBlock Margin="5">Enter new Value</TextBlock>
<TextBox Name="txtValue" Margin="5"/>
</StackPanel>
</Border>
<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.ItemTemplate7gt;
</ListBox>
<Button Name="btnOK" Margin="10" Width="75"7>OK7lt;/Button>>
</StackPanel>
</Window>
The complete source code for this project is very long, so it is not shown here, but it can be downloaded from the link above. Here is the output of the visualizer, where you can see the value of a Color
type variable: