Click here to Skip to main content
Click here to Skip to main content

Woodstock for WPF

, , , 31 Dec 2007
Rate this:
Please Sign up or sign in to vote.
A debugger visualizer which allows you to inspect the visual tree.

Foreword

This article presents a debugger visualizer for WPF called Woodstock. After Woodstock was released to the world, it was used as a prototype to design and develop a superior visualizer called Mole v4 For Visual Studio - With Editing. We recommend that you use Mole v4 For Visual Studio - With Editing as your primary WPF and Visual Studio visualizer, because its functionality is a superset of what Woodstock has to offer. However, if you are interested in learning about the evolution of Mole by seeing what came before it, be sure to read this article.

Table of Contents

Introduction

This article presents a Visual Studio debugger visualizer called "Woodstock" which enables you to view the visual tree. It provides detailed information about all properties on every element in the visual tree, and a snapshot image of each element, allowing you to more easily debug complex WPF user interfaces.

Background

Every WPF developer knows and loves Snoop, the powerful free utility created by Pete Blois. Amongst other things, Snoop allows you to view the visual tree of any WPF application, and inspect the properties of any element. This is a huge time-saver, and a great way to learn about WPF.

Despite the tremendous powers of Snoop, it does not really mesh well with debugging WPF code in Visual Studio. Snoop is a separate application which attaches itself to a WPF application's process. You cannot step through WPF code and use Snoop on that UI at the same time. I, Josh, often found myself debugging some WPF code and wishing that Snoop somehow worked in Visual Studio. That's why Woodstock was born.

I created a small subset of Snoop's functionality, and implemented it as a debugger visualizer. Since the uber-visual tree inspection application is called "Snoop", I decided to name my little visualizer "Woodstock". In case you are not familiar with Peanuts, here's the lowdown on Woodstock.

Josh's Contribution

Throughout the article, when you see the word "I", it is referring to me, Josh Smith. I created Woodstock and wrote this article about it. The burden of updating the code, and this article, falls squarely on my shoulders. Woodstock is my invention, but it would not be nearly as useful and efficient without the numerous suggestions and bug reports I received from dozens of people across the globe!

Karl's Contribution

Karl Shifflett has been a huge help in making Woodstock a great debugging tool. Ever since I published the initial article, he has made many great suggestions, sent me code snippets, reported bugs, etc. To thank him and show my appreciation, I decided to add him as an author of this article.

Florian's Contribution

Shortly after this article was published, Florian Kruesch published an article about a visualizer which shows you an image of the element you're inspecting in the debugger. We agreed to merge his visualizer and article into Woodstock. Karl and I changed his code a bit so that it would work well in Woodstock, but the initial idea of an image visualization came from Florian.

Installing Woodstock

If you just want to download the visualizer and install it, follow these steps:

  1. At the top of this article, click the "Download the visualizer" link.
  2. Save the ZIP file to disk and extract it.
  3. Place "JoshSmith.WpfVisualizer.Woodstock.dll" at one of these locations:
    1. VS install path\Common7\Packages\Debugger\Visualizers
    2. My Documents\Visual Studio 2005\Visualizers

Instead, if you click the "Download the code and demo" link, you will get the Woodstock.zip file, in which there are two Visual Studio solutions. The one called "WpfVisualizer" contains the Woodstock visualizer code. The other, "WpfVisualizerTestApp", is a demo app which allows you to try out Woodstock. Be sure to drop the visualizer DLL into your Visualizers directory first before running the test app.

If you have any trouble installing the visualizer, there is a bleak amount of documentation on how to do so here.

Visual Studio Version Information

If you are using this visualizer in Visual Studio 2005, then be sure to download the DLL which was compiled for VS2005. It turns out that the Microsoft.VisualStudio.DebuggerVisualizers.DLL assembly has a different version for VS2008, so I made a separate build of Woodstock for VS2008 users. The source code and demo app are compiled against VS2005 with the Orcas extensions, so that people who have not yet switched to VS2008 can use it too.

However, if you decide to build Woodstock in VS2008, be sure to remove the reference to Microsoft.VisualStudio.DebuggerVisualizers.DLL and add a reference to version 9.0.0.0 of that assembly. If you do not, you will get a nasty exception when trying to use Woodstock.

Making Woodstock Work with XBAPs

One big advantage Woodstock has over Snoop is that you can use it to debug XBAP applications. Here is what you need to do to enable that:

  1. Right-click on the XBAP project in Solution Explorer and open the Properties page.
  2. Under the Security tab, select the "This is a full trust application" radiobutton.
  3. Be sure to set it back later on to partial trust in order to properly test deployment.

Thanks to CodeProject member "ivolved" for that tip!

How to Use Woodstock

You can use Woodstock just like any other debugger visualizer. Here's what the datatip looks like when you mouse over a DependencyObject-derived object after hitting a breakpoint:

When you open the Woodstock visualizer, it looks like this:

The TreeView on the left represents the entire visual tree of the UI you are debugging. Initially, the element which you hovered the mouse over in the code editor will be selected in the TreeView, but you are free to select any element in the visual tree. If an element has its Name property set, then that name will appear next to the element's type. Each element that has descendants has the number of descendants displayed beside it in parentheses.

The DataGridView on the right shows all of the properties of the selected element. If a property is a dependency property, then the "Value Source" column shows what is providing the effective value of that property (i.e., is it a locally provided value? a value provided by a template? a Style? a system theme? etc). Having the source of a dependency property's value can make it much easier to track down issues where one property might be set from any number of external influences (which is the heart and soul of DPs).

Clicking on a button to the left of a property name will copy a Google search query to your clipboard, so that you can paste that URL into a Web browser's address box and research the property. I wanted to just open a Web browser to that URL, but attempting to do so always made Visual Studio crash when running on Windows XP (but, oddly enough, not Vista).

You can filter the properties shown in the grid by typing a case insensitive filter string into the TextBox toward the bottom of the Form. If you do not want to view the attached properties, simply uncheck the CheckBox next to the TextBox. If you only want to view attached properties, leave the CheckBox checked and type "." as your filter text string. Wink | ;)

When you click on the "Selected Element Snapshot" tab, you will see an image of the element currently selected in the TreeView. Here's a screenshot of that:

UIElements and FrameworkElements must be initialized and loaded before they can be displayed. As a result, when you put a breakpoint in, for example, a Window's constructor, you won't see anything if you check out the visuals in that Window.

In case you have never used a debugger visualizer before and want some more information, read this page in the docs.

Limitation

There is one issue which might possibly make using Woodstock difficult. Visual Studio's "Debugger Visualizer" feature only allows a visualizer to have a brief amount of time to serialize its data. If your visual tree is huge, or your machine is bogged down processing other things, etc., then Woodstock might take too long and it will time out. Visual Studio will show an error dialog stating: "Function evaluation timed out."

I did my best to work around this limitation by optimizing the way that information about the visual tree is retrieved and represented. Before making the optimizations, I tested Woodstock against an application with a gigantic visual tree and long-running background processes, and it timed out frequently. After making the optimizations, it never timed out. Hopefully, those optimizations will prevent you from seeing that annoying error message, too.

How it Works

Debugger visualizers are strange little creatures. You have to keep in mind that there are two logical parts involved: debugger-side code and debuggee-side code. Visual Studio's debugger allows you to inject some code into itself and the process being debugged. The code in the debugger process shows the visualizer UI. The code in the process being debugged allows you to package up the data that you want to display in the visualizer. Once that data has been serialized, it gets shipped over to your visualizer in the debugger process, at which point you can turn it back into live objects and display them. For more information about the architecture of visualizers, read this article in the SDK.

Basic Architecture

A visualizer must specify a class for which it provides visualizations, including subclasses of that class. We want our visualizer to work for any DependencyObject, or subclass thereof. The problem is that DependencyObject is not serializable, so we cannot rely on the standard serialization behavior of the debugger visualizer framework to make life nice and easy for us. Instead, I created a class which intercepts requests to serialize the visual tree, and then created a parallel hierarchy of serializable objects which contain the visual tree information we want to display in the visualizer. Here is that class:

/// <summary>
/// Serializes the element tree into DTOs.
/// </summary>

public class ElementTreeVisualizerObjectSource : VisualizerObjectSource
{
    public override void GetData(object target, Stream outgoingData)
    {
        DependencyObject depObj = target as DependencyObject;

        // This is the magic line of code which creates a serializable
        // representation of the entire visual tree of which the target
        // element is a member. The element property information and
        // snapshots are not created at this time. 
        WpfElementTree elementTree = new WpfElementTree(depObj);
        _binaryFormatter().Serialize(outgoingData, elementTree);
    }
    // Other methods omitted for now...
}

The visualizer itself is a simple class which only shows a custom WinForms Form, as seen below:

/// <summary>
/// A debugger visualizer for WPF elements.
/// </summary>

public class ElementTreeVisualizer : DialogDebuggerVisualizer
{
    protected override void Show(IDialogVisualizerService windowService, 
    IVisualizerObjectProvider objectProvider)
    {
        using (Form displayForm = new ElementTreeVisualizerForm(objectProvider))
        windowService.ShowDialog(displayForm);
    }
}

In the AssemblyInfo.cs file, an attribute is applied which Visual Studio uses to figure out what type(s) this visualizer is used for, as well as some other information. That attribute is seen below:

[assembly: System.Diagnostics.DebuggerVisualizer(
typeof(WpfVisualizer.ElementTreeVisualizer),
typeof(WpfVisualizer.ElementTreeVisualizerObjectSource),
Target = typeof(System.Windows.DependencyObject),
Description = "Visual Tree Visualizer (Woodstock)")]

Getting Information About All of the Properties

One piece of the code which took me (Josh) a while to get right was the logic which stores information about all of an element's properties. Information about a property is stored in an instance of my WpfElementProperty struct. A WpfElement object has a WpfElementPropertyList which stores information about every property of that element. The logic which populates that list is seen below:

private void Initialize(DependencyObject propertySource)
{
    PropertyDescriptorCollection props = 
            TypeDescriptor.GetProperties(propertySource);

    foreach (PropertyDescriptor prop in props)
    {
        PropertyDescriptor resolvedProp = prop;
        string baseValueSource = null;

        DependencyPropertyDescriptor depProp = 
        DependencyPropertyDescriptor.FromProperty(prop);

        if (depProp != null)
        {
            baseValueSource = DependencyPropertyHelper.GetValueSource(
            propertySource, depProp.DependencyProperty).BaseValueSource.ToString();
            resolvedProp = depProp;
        }

        string name = resolvedProp.Name;
        object value = resolvedProp.GetValue(propertySource);
        base.Add(new WpfElementProperty(name, value, baseValueSource));
    }
}

Lazily Loading Element Information

A far more challenging thing to implement was the logic which lazily loads the property information and snapshot image of each element in the visual tree. Initially, I serialized all of that information for every element in the tree in the ElementTreeVisualizerObjectSource's GetData method, seen above. It turns out that Visual Studio has a timeout monitor for that method (deadlock detection?), which caused the method to be terminated before it was able to complete serializing the data for large visual trees. When the timeout occurs, the Woodstock UI does not appear, and the visualizer is unusable.

I worked around this problem by redesigning the way that the property information and the snapshot image are created for each WpfElement instance. Instead of creating them all in one fell swoop, I create them on an as-needed basis. This prevents all of that data from being serialized at once, which makes the GetData method lightning fast (so it won't timeout).

When the WpfElementTree is created in the ElementTreeVisualizerObjectSource.GetData method, the WpfElement instances do not have any of that extra information. When the user selects an element in Woodstock's TreeView, it checks to see if the newly selected WpfElement is already populated with the property information and snapshot data. If not, we send that element over to the debuggee-side code and expect it to be returned with the missing data. That code is in the ElementTreeVisualizerForm class, as seen below:

void treeView_AfterSelect(object sender, TreeViewEventArgs e)
{
    WpfElement element = (WpfElement)e.Node.Tag;

    if (!element.IsPopulated)
    {
        WpfElement populatedElement = this.CreatePopulatedElement(element);
        e.Node.Tag = populatedElement;

        if (e.Node.Parent != null)
        {
            // Give the parent element a reference to the new child.
            WpfElement parentElement = (WpfElement)e.Node.Parent.Tag;
            parentElement.ReplaceChild(element, populatedElement);
        }

        element = populatedElement; 
    }
    
    this.RefreshGrid(element);
    this.RefreshImage(element);
}

The method which actually requests that the ElementTreeVisualizerObjectSource populates the WpfElement is seen here:

WpfElement CreatePopulatedElement(WpfElement element)
{
    // Since serializing and deserializing all of this element's 
    // children might take a very long time, remove them for now
    // and we'll add them back in later.
    List<WpfElement> children = new List<WpfElement>(element.Children);
    element.Children.Clear();

    // Ask the ElementTreeVisualizerObjectSource, 
    // which is running in the debuggee process,
    // to fill up this element's property info 
    // and take a snapshot of it.
    MemoryStream inputStream = new MemoryStream();
    _binaryFormatter.Serialize(inputStream, element);
    Stream outputStream = _objectProvider.TransferData(inputStream);
    inputStream.Close();

    // Get the populated WpfElement back from the debuggee process.
    WpfElement populatedElement = 
        (WpfElement)_binaryFormatter.Deserialize(outputStream);

    // Add the element's children back to its list.
    populatedElement.Children.AddRange(children);

    // Keep a reference to the snapshot image so 
    // that we can easily dispose of it later.
    _elementSnapshots.Add(populatedElement.Snapshot);

    return populatedElement;
}

When the object provider's TransferData method is invoked, it results in the ElementTreeVisualizerObjectSource's TransferData method to be called back in the debuggee process. That logic is seen below:

/// <summary>
/// The visualizer calls this method when an element's property information
/// and snapshot is needed to be displayed in the UI.
/// </summary>

public override void TransferData(
    object target, Stream incomingData, Stream outgoingData)
{
    WpfElement element =(WpfElement)_binaryFormatter.Deserialize(incomingData);
    Debug.Assert(!element.IsPopulated, "Element should not be populated.");
    WpfElement populatedElement = this.CreatePopulatedElement(element);
    _binaryFormatter.Serialize(outgoingData, populatedElement);
}

The method which populates the WpfElement with property information and a snapshot is seen here:

WpfElement CreatePopulatedElement(WpfElement element)
{
    DependencyObject depObj = _elementTree.GetDependencyObjectByID(element.ID);
    WpfElementPropertyList properties = new WpfElementPropertyList(depObj);
    Bitmap snapshot = VisualSnapshot.TakeSnapshot(depObj);
    element.Populate(properties, snapshot);
    return element;
}

One crucial thing to notice here is that we have an association between the real WPF element and a WpfElement instance which represents it. That link is formed by a simple integer ID value. WpfElement has an ID property, and the real WPF element is put into a Dictionary<int, DependencyObject>, where the integer key is the ID value.

The two ID values are set up in the WpfElement constructor:

private WpfElement(
 DependencyObject currentElem, 
 DependencyObject initialElem, 
 Dictionary<int, DependencyObject> elementMap)
{
    // Create an association between this object and the real WPF object
    // that it represents by giving them both the same ID.
    _id = WpfElement.elementCount++;
    elementMap.Add(_id, currentElem);
    // Other code elided...
}

When the debugger process sends over a WpfElement to be populated, the element's ID is used to look up the real WPF object in the dictionary.

Taking Snapshots

The image processing code can be found in the VisualSnapshot class. Its primary method looks like this:

public static Bitmap TakeSnapshot(object target)
{
    Bitmap bitmap = null;
    if (target is BitmapSource)
    {
        bitmap = BitmapSourceToGdiImage(target as BitmapSource);
    }
    else if (target is FrameworkElement)
    {
        FrameworkElement fe = target as FrameworkElement;
        if (0 < fe.ActualWidth && 0 < fe.ActualHeight)
        {
            BitmapSource bitmapSource = CaptureVisual(fe);
            bitmap = BitmapSourceToGdiImage(bitmapSource);
        }
    }

    if (bitmap == null)
    {
        // Return a new Bitmap so that the cached instance 
        // does not get disposed of somewhere else.
        return new Bitmap(VisualSnapshot.UnavailableSnapshot);
    }
    else
    {
        // Return a new Bitmap because we can get a 
        // "corrupt memory" exception otherwise.
        using (bitmap)
            return new Bitmap(bitmap);
    }
}

Revision History

  • November 12, 2007 – Created the article.
  • November 13, 2007 - Added the "Visual Studio Version Information" section, and provided a VS2008 version of the Woodstock assembly.
  • November 13, 2007 - Improved performance by changing WpfElementProperty to a struct instead of a class. Added logic which ensures the display order of columns in the DataGridView. Added a number to every non-leaf node in the TreeView indicating how many descendants that element has. Updated all of the file downloads.
  • November 14, 2007 - Improved the way that an element's properties are discovered, thanks to a tip from Andrew Smith at Infragistics. Now that code is faster, and all attached properties will be included as well. Added the "Limitation" section. Updated all of the file downloads.
  • November 15, 2007 - After receiving tons of great feedback and encouragement, I added several features: the property filtering TextBox, the CheckBox which allows you to hide the attached properties in the grid, and a column of links which copy a Google search to your clipboard. Also, I made some optimizations to the serialization code, hoping to avoid the dreaded "Function evaluation timed out" error message. I updated all of the file downloads.
  • November 15, 2007 - Fixed the way that Google search URLs are created for attached properties. Updated the file downloads.
  • November 15, 2007 - Thanks to Karl Shifflett, I discovered a bug which only exists on Vista where exceptions are thrown as soon as Woodstock was opened. To fix the problem, I changed the "Property Name" column to use buttons instead of links. That solved the weird remoting/GDI+ issue. I updated the file downloads.
  • November 15, 2007 - Wow, a lot of updates today! I added in Florian Kruesch's code and added him as an author of this article. Adding in his code pointed out a bug in my logic where DependencyObjects that are not in a visual tree were being handled incorrectly. That has been fixed. I updated the file downloads.
  • November 16, 2007 - I implemented a great suggestion from Karl Shifflett, so that the Woodstock window appears immediately and shows a "splash screen" while the visual tree information is created on a worker thread. This really improves the responsiveness of the UI, making Woodstock more usable. I also cleaned up the code a bit and added more comments. I updated the file downloads.
  • November 17, 2007 - I made the element information lazy-loaded to vastly improve performance and avoid timing out. I added the "Lazily Loading Element Information" subsection. Doing this also enabled me to provide a snapshot of every element in the tree, not just the one the user initially selected. Also, I fixed a few bugs and gave the snapshot image some scrollbars so that you can view the entire image without having to resize the window (thanks to Rick Eberle for the suggestion). I updated all the file downloads.
  • November 18, 2007 - I got rid of the attached ID property for elements in the visual tree, and instead use a dictionary to associate a WpfElement with a DependencyObject. Thanks to Karl Shifflett for pointing out that simpler and more efficient way of creating the object associations. I updated the file downloads and modified the "Lazily Loading Element Information" subsection.
  • November 18, 2007 - I cleaned up the code a bit and updated the relevant snippets in the article. Added the "Making Woodstock Work with XBAPs" section. Updated the file downloads.
  • November 19, 2007 - I implemented performance optimizations after performing stress tests. Now the nodes in the TreeView are loaded on-demand, and the children of a WpfElement are temporarily removed when it is being sent over to the debuggee process to be populated. I updated the file downloads.
  • November 20, 2007 - Karl Shifflett fixed the visual snapshot code so that the elements are not clipped. I then took his code and simplified it a little bit and put it into Woodstock. Due to his amazing efforts in this project, I decided to add him as an author of this article! I updated the file downloads.
  • November 20, 2007 - Dropped a new build of Woodstock which was compiled in the RTM of VS2008. Also, I removed the images of Woodstock which I got from the Peanuts web site, because some weenies were whining about it, and one guy even threatened to report me to whoever officially owns those images (geez, talk about a buzz-kill...). All the files were updated.
  • November 21, 2007 - I fixed a bug in the snapshot image logic, and updated the file downloads.
  • November 23, 2007 - I put in a bug fix which Karl Shifflett pointed out in the VisualCapture class. Elements whose ActualWidth or ActualHeight is between 0.0 and 1.0 were causing an exception to be thrown because the corresponding dimension of the Bitmap for that element has an invalid value of 0. I updated the file downloads.

License

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

About the Authors

Josh Smith
Software Developer (Senior) Cynergy Systems
United States United States
Josh creates software, for iOS and Windows.
 
He works at Cynergy Systems as a Senior Experience Developer.
 
Read his iOS Programming for .NET Developers[^] book to learn how to write iPhone and iPad apps by leveraging your existing .NET skills.
 
Use his Master WPF[^] app on your iPhone to sharpen your WPF skills on the go.
 
Check out his Advanced MVVM[^] book.
 
Visit his WPF blog[^] or stop by his iOS blog[^].
Follow on   Twitter

Karl Shifflett
Architect Gayle Manufacturing Company
United States United States
Karl loves .NET, WPF, WCF, ASP.NET, VB.NET and C#.
 
Awards:
 
  • December 2008 VB.NET Code Project Article Award
  • 2009 Code Project MVP
  • 2008 Code Project MVP
  • 2008 Microsoft MVP - Client App Dev
  • December 2007 VB.NET Code Project Article Award
  • Gold Medal Winner at IBM's 1998 PROIV Programming Contest in Las Vegas
Click here to check out my Blog
 
Click here to learn about Mole 2010 debugging tool for Visual Studio 2010
 
Click here to read about XAML Power Toys
 

Just a grain of sand on the worlds beaches.

Follow on   Twitter

Florian Kruesch
Web Developer
Germany Germany
Florian Krüsch works as a Freelance Software Architect, Developer and Consultant in Düsseldorf, Germany.
 
He is excited about WPF, Silverlight and ASP.net as well as Enterprise Development on the .NET platform.
 
When not thinking, coding or listening to podcasts, he enjoys spending his time with his family and his little son Nicki.
 


Comments and Discussions

 
GeneralRe: Thanks :) PinmvpJosh Smith16-Nov-07 4:27 
GeneralGreat stuff! PinmemberR.Eberle16-Nov-07 3:39 
GeneralRe: Great stuff! PinmvpJosh Smith16-Nov-07 3:57 
GeneralRe: Great stuff! PinmvpJosh Smith17-Nov-07 16:02 
GeneralRe: Great stuff! PinmemberR.Eberle19-Nov-07 2:45 
GeneralThank you... PinprotectorMarc Clifton14-Nov-07 12:42 
GeneralRe: Thank you... PinmvpJosh Smith14-Nov-07 13:20 
GeneralRe: Thank you... PinmemberSacha Barber15-Nov-07 0:12 
GeneralRe: Thank you... PinprotectorMarc Clifton15-Nov-07 0:46 
JokeRe: Thank you... PinmvpJosh Smith15-Nov-07 0:53 
GeneralRe: Thank you... PinprotectorMarc Clifton15-Nov-07 1:03 
GeneralRe: Thank you... PinmemberSacha Barber15-Nov-07 9:23 
GeneralRe: Thank you... PinmemberSacha Barber15-Nov-07 9:22 
GeneralRe: Thank you... Pinmemberivolved15-Nov-07 4:34 
GeneralRe: Thank you... PinmemberSacha Barber15-Nov-07 9:21 
GeneralRe: Thank you... PinmemberKenGuru18-Nov-07 23:26 
GeneralRe: Thank you... PinmemberSacha Barber19-Nov-07 0:39 
Generalwoodstock and xbap Pinmemberrkgeorge13-Nov-07 4:49 
GeneralRe: woodstock and xbap PinmvpJosh Smith13-Nov-07 5:44 
GeneralRe: woodstock and xbap Pinmemberivolved14-Nov-07 9:09 
GeneralRe: woodstock and xbap PinmvpJosh Smith14-Nov-07 9:12 
GeneralCOOOLLLL Pinmembermarlongrech13-Nov-07 4:21 
GeneralRe: COOOLLLL PinmvpJosh Smith13-Nov-07 4:26 
GeneralCreative and Outstanding PinmemberKarl Shifflett13-Nov-07 0:41 
GeneralRe: Creative and Outstanding PinmvpJosh Smith13-Nov-07 1:34 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 31 Dec 2007
Article Copyright 2007 by Josh Smith, Karl Shifflett, Florian Kruesch
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid