Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

Writing a Debugger Visualizer in WPF: Part 1

Rate me:
Please Sign up or sign in to vote.
4.67/5 (7 votes)
15 Apr 2010CPOL6 min read 27.2K   2   19   5
This article describes writing a debugger visualizer in WPF.

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:

Image 1

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.

Image 2

There is only one method to overload in the DialogDebuggerVisualizer class, and here is the signature of that method:

C#
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.

Image 3

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:

Image 4

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:

C#
protected override void Show(IDialogVisualizerService windowService, 
          IVisualizerObjectProvider objectProvider)
{

    // set the attributes of WPF window
    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.

Image 5

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.

C#
[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:

C#
// 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;
}

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:

C#
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.

C#
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.

C#
// This function is only for debugging purpose
public static void TestShowVisualizer(object obj)
{
    VisualizerDevelopmentHost host = 
       new VisualizerDevelopmentHost(obj, typeof(MyVisualizerClass));
    host.ShowVisualizer();
}

Here is the complete code for this visualizer:

C#
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)
        {
            // set the attributes of WPF window
            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();
        }

        // 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 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, 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:

C#
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.

Image 6

It is also available at the watch window, as shown in this screenshot:

Image 7

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:

XML
<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.

C#
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:

C#
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:

C#
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();
        }


        // 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 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; }
    }
}

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:

Image 8

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Team Leader American Institute for Research
United States United States
Working as a Team leader in American Institute for Research

Comments and Discussions

 
QuestionKind of the file Pin
Member 1179148924-Jun-15 23:36
Member 1179148924-Jun-15 23:36 
QuestionDLL Pin
Member 1179148924-Jun-15 23:08
Member 1179148924-Jun-15 23:08 
QuestionThrows exception after closing visualizer window in VS2013 Pin
DmytroSokhach19-Apr-14 10:16
DmytroSokhach19-Apr-14 10:16 
Hello Zeeshan,
I've downloaded the source code and run it on VS2013. Visualizer itself works, however closing it's window throws the exception below.
System.CannotUnloadAppDomainException was unhandled
  HResult=-2146234347
  Message=Error while unloading appdomain. (Exception from HRESULT: 0x80131015)
  Source=mscorlib
  StackTrace:
       at System.AppDomain.Unload(AppDomain domain)
       at Microsoft.VisualStudio.DebuggerVisualizers.DebugViewerShim.ManagedShim.Microsoft.VisualStudio.DebuggerVisualizers.DebugViewerShim.IManagedViewerHost.CreateViewer(IntPtr hwnd, Object hostServicesParam, IPropertyProxyEESide proxy)
       at MyVisualizer.MyColorClass.TestShowVisualizer(Object obj) in d:\_samples\MyVisualizer_Part_2_src\MyVisualizer\MyVisualizer\MyVisualizer.cs:line 137
       at MyVisualizerClient.Program.Main(String[] args) in d:\_samples\MyVisualizer_Part_2_src\MyVisualizer\MyVisualizerClient\Program.cs:line 29
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 

The worst thing is that if it's installed the VS2013 stucks for 10-20 seconds and then shows notification window with message:
"Could not load this custom viewer."

Also a friend of mine tried this on VS2012 and it's fine.

Please reply if you might have an answer how to fix the exception in VS2013. Thanks in advance.
QuestionWhy are you going to such trouble to show the window? Pin
Matthew Emory20-Feb-14 7:39
Matthew Emory20-Feb-14 7:39 
GeneralGood information Pin
ronnyek15-Apr-10 12:50
ronnyek15-Apr-10 12:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.