Expanding WPF for UI Debugging

The Optimum User Experience

Since the very earliest days of WPF/XAML development, there has always been a need to inspect the visual tree. Early Pre-Alpha builds of Blend had a debug command which would dump the entire visual tree to a text file so developers could look through it for anomalies. We had internal tools for inspecting visual trees and the properties of those elements and debugging capabilities of this type have been a frequent ask for years. For Visual Studio 2015, we wanted to take a look at the needs that developers, both internal and external, faced and look at the tools we’ve made to create the optimal runtime inspection tools that we could. Information on how to use the UI debugging tools can be found here, but if you’re interested in how we made it happen, please read on.

After examining our internal solutions and the common issues we’ve heard and put together the following list of items we wanted to do:

  • The ability to inspect an existing installed app or an ‘in-development’ app at debug time.

  • A representation of the visual tree that is always up to date.

  • The ability to select elements by selecting them in the running app.

  • The ability to locate and select any element in the representation of the visual tree.

  • A command to locate a given element in the source XAML.

  • A complete list of all properties on any selected element, displayed based on where the property is set (locally, in a Style, etc…).

  • The ability to change any property setter at any scope.

  • A command to jump to wherever a property setter is defined if it comes from XAML.

  • A persistence option to push runtime debugging changes into the XAML.

We created these requirements based on the needs of XAML developers, regardless of their platform or target device of choice. Naturally, the experience would tweak slightly based on these factors. The way a user selects an element for investigation on the desktop is going to be different from selection on a Windows Phone.

What do we need and what is missing?

The first system we need is one that allows us to talk to different apps on different platforms. We started by creating the XamlDiagnosticTap.dll, which is a generic solution that the debugger extension in Visual Studio knows how to communicate with. This assembly is loaded by one of a set of platform specific assemblies. For instance, in a WPF application, WPFXamlDiagnosticTap.dll is injected into the running app and it will pull in the generic XamlDiagnosticTap.dll. It’s obvious that we’ll see other flavors of the platform level tap, like DesktopXamlDiagnosticTap.dll or WPXamlDiagnosticTap.dll.

 

When we compared our requirement list to the capabilities of WPF, we found that we really only had two shortcomings. WPF didn’t provide a system for listening to changes in the visual tree, and it only stored source information privately in BAML files.

The VisualTreeChanged event

It turns out that all Visual Tree manipulations channel through four or five specific methods. These methods had all the information that we wanted, including the handles to the elements being added or removed. Adding the events here was surprisingly simple. The final implementation fires the events whenever any element is added or removed from any other element.

The API ends up looking like this:

public enum VisualTreeChangeType { Add, Remove }

public static event EventHandler<VisualTreeChangeEventArgs> System.Windows.Diagnostics.VisualDiagnostics.VisualTreeChanged

public class VisualTreeChangeEventArgs : EventArgs

{

  public DependencyObject Parent { get; }

  public DependencyObject Child { get; }

  public int ChildIndex { get; }

  public VisualTreeChangeType ChangeType { get; }

}

It should be noted that this API is designed as a debugger API. Specifically, when you attempt to register for notifications on the VisualTreeChanged event, the request to register is ignored if the debugger is not currently attached. It’s also worth mentioning that at the time of this writing, this API is still in development, and it’s subject to change.

The GetXamlSourceInfo method

The source code information was a bit trickier. For elements originating from a BAML file, the elements already knew their source information. They knew the source XAML file that declared the elements as well as the Line/Column information to locate the declarations. Unfortunately, the information was private to the element and couldn’t be extracted by the diagnostic tap. Then, unfortunately, elements originating from a XAML file didn’t have their source information at all.

Exposing the already existing file information for BAML elements was easy, but we started some interesting discussions around the XAML elements. BAML is an optional step, and many people don’t even use it. Additionally, while folks are still working on their app, it’s uncommon for people to already be creating BAML files, so in our investigations, most people worked from XAML files until later in the development process. Now, consider what it means to add the source information. For every element, we need to add the file information along with the line/column information. While this isn’t a large amount per element, when you add it all together, it’s a sufficiently significant amount of memory that may obscure other memory issues. We eventually decided that the source information for XAML originating elements would only be available in Debug builds of the user’s apps, and we’d require a launch-time switch to enable it, even for Debug builds.

For DependencyObject elements, it’s trivial to add an additional attached DependencyProperty, and in that property, store the file, line and row information. However, non-DependencyObject elements, like Styles, don’t afford us this luxury. For these, we used the handy ConditionalWeakTable object from the System.Runtime.CompilerServices namespace. Simply put, the ConditionalWeakTable is similar to a dictionary, but with the noted exception that it doesn’t hold references to the objects inside it. This means that if we create a reference to Object A and all other references to A cease to exist, the ConditionalWeakTable deletes the reference to A. This prevents the ConditionalWeakTable from keeping objects alive once the system has released all other references to them.

In effect, an element gets created, we store its source information in the ConditionalWeakTable. If the app deletes the element and abandons all of its references to it, the ConditionalWeakTable automatically removes the source information from itself and the object is ready for Garbage Collection. More information on the ConditionalWeakTable can be found on MSDN.

The API for getting XamlSourceInfo is quite straightforward:

  public static XamlSourceInfo System.Windows.Diagnostics.VisualDiagnostics.GetXamlSourceInfo(object obj)

  public class XamlSourceInfo

  {

    public Uri SourceUri { get; }

    public int LineNumber;

    public int LinePosition;

  }

However, in order to use to use the method, two things need to be true first. As discussed above, the SourceInfo is only available for Debug builds of apps. Second, the environment variable ENABLE_XAML_DIAGNOSTICS_SOURCE_INFO will be checked when the app is started. If the variable is set and set to any value except 0 or false, WPF will store the source information for all elements. When launching your app from inside Visual Studio, the debugger extension will automatically set this in the process it will use to launch your app. However, if you’re trying to get source information on another machine, you can manually set the environment variable before launching the Debug build of the app, and you will be able to attach and get source information.

Additionally, like the VisualTreeChanged event, the GetXamlSourceInfo API is subject to change. MSDN will have the latest up to date information when the API is finalized.

Conclusion

The architecture for these tools had to be created in a platform agnostic way, in order to have a consistent experience across platforms. Then, we needed to create a platform dependent wrapper around our generic architecture to handle the various differences between the platforms. Finally, when we looked at the user experience we wanted to deliver, we had to choose between cutting back on the experience or extending the platform. When it came down to it, we simply weren’t satisfied with the experience we’d be delivering to our WPF developers without the platform extensions. The decision to deliver the top-notch experience that we wanted was an easy one to make.

Dante Gagne is a Program Manager with 12 years of experience with XAML. He started as one of the original testers on Expression Blend and brings his passion to XAML developers in Visual Studio. His focus is on the design experience and productivity.