|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
t
ForewordThis 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
IntroductionThis 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. BackgroundEvery 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 ContributionThroughout 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 ContributionKarl 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 ContributionShortly 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 WoodstockIf you just want to download the visualizer and install it, follow these steps:
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 troubles installing the visualizer, there is a bleak amount of documentation on how to do so here. Visual Studio Version InformationIf 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 XBAPsOne 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:
Thanks to CodeProject member "ivolved" for that tip! How to Use WoodstockYou can use Woodstock just like any other debugger visualizer. Here's what the datatip looks like when you mouse over a
When you open the Woodstock visualizer, it looks like this:
The The 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 When you click on the "Selected Element Snapshot" tab, you will see an image of the element currently selected in the
In case you have never used a debugger visualizer before and want some more information, read this page in the docs. LimitationThere 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 WorksDebugger 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 ArchitectureA visualizer must specify a class for which it provides visualizations, including subclasses of that class. We want our visualizer to work for any /// <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 /// <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 PropertiesOne 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 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 InformationA 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 I worked around this problem by redesigning the way that the property information and the snapshot image are created for each When the 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 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 /// <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 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 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 SnapshotsThe image processing code can be found in the 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
| ||||||||||||||||||||