Tag Archives: Visual Tree

Debugging visual tree/layout issues in Silverlight and on Windows Phone

Many times, especially when you are doing dynamic layouts, dealing with bindings and DataTemplates – you start scratching your head trying to figure out why your control is not showing up where you want it to show, how to fix it or simply what are the coordinates of some point. You can look through your code, but sometimes the problem is not in your code or it just is not clear.

In such situations it often helps to debug the visual tree of the application and you can do it by browsing the object tree in Visual Studio, but it is sometimes quite laborious since you would usually have to call VisualTreeHelper.GetParent many times before you get to where the problem lies – if you do not get confused on the way and give up. In WPF – I used to use Snoop or Mole. In Silverlight – I did not hear of such tool (I just found about Silverlight Spy today – but it is not free if you want to do anything with Windows Phone). To help solve it I used to write or copy&paste a small attached dependency property that makes the VisualTreeHelper.GetParent calls for me and outputs the interesting properties when a control gets loaded. Today I finally decided to clean it up a bit, make it better and share it with you, so here it is.

Download the sample project here

http://dl.dropbox.com/u/1192076/VisualTreeDebugger.zip

This is just a Windows Phone (Mango) panorama project straight from the template with my VisualTreeDebugger class added and two breakpoint properties defined on some TextBlocks. One will break each time the DataTemplate that includes the TextBlock is applied and the TextBlock loaded, the other one – when you tap on one of the TextBlocks.

How do I use it?

Simply add the VisualTreeDebugger class to your own project, spacify the xmlns reference in XAML and either set VisualTreeDebugger.BreakOnLoaded="True" on your control to have the debugger break when the control first loads or VisualTreeDebugger.BreakOnTap="True" to break when you tap that control. Then just run it from your Visual Studio with debugger attached (F5).

<!--Panorama control-->
<controls:Panorama
    Title="my application">
    <controls:Panorama.Background>
        <ImageBrush
            ImageSource="PanoramaBackground.png" />
    </controls:Panorama.Background>

    <!--Panorama item one-->
    <controls:PanoramaItem
        Header="first item">
        <!--Double line list with text wrapping-->
        <ListBox
            Margin="0,0,-12,0"
            ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel
                        xmlns:local="clr-namespace:VisualTreeDebugger"
                        Margin="0,0,0,17"
                        Width="432"
                        Height="78">
                        <TextBlock
                            local:VisualTreeDebugger.BreakOnTap="True"
                            Text="{Binding LineOne}"
                            TextWrapping="Wrap"
                            Style="{StaticResource PhoneTextExtraLargeStyle}" />
                        <TextBlock
                            local:VisualTreeDebugger.BreakOnLoaded="True"
                            Text="{Binding LineTwo}"
                            TextWrapping="Wrap"
                            Margin="12,-6,12,0"
                            Style="{StaticResource PhoneTextSubtleStyle}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </controls:PanoramaItem>
</controls:Panorama>

 

What do I get

You get a “path” field which is a list of all dependency objects between your debugged element and the visual tree root. It makes it easy to quickly get access to any of those objects to inspect their properties, when you debug it.

image

image

In the Output/Debug (Ctrl+W, O) window for each of these elements – you get a trace of selected layout properties that may affect the layout, including type, name, actual dimensions and position, logical properties like Width, Height, alignments, Margin and Padding, DataContext and brush information to sweeten the deal.

path[22] is Control: Microsoft.Phone.Controls.Panorama():
    ActualWidth=480
    ActualHeight=800
    Position – X=0, Y=0, Right=480, Bottom=800
    DataContext: VisualTreeDebugger.MainViewModel HashCode: 74304488
    Width=NaN
    Height=NaN
    HorizontalAlignment=Stretch
    VerticalAlignment=Stretch
    Margins=0,0,0,0
    Background=ImageBrush: System.Windows.Media.Imaging.BitmapImage, UriSource: PanoramaBackground.png
    Foreground=SolidColorBrush: #FFFFFFFF

How does it help?

You can find that control somewhere in the dozens of elements of your visual tree that has the alignment or margins set incorrectly or screen position of some invisible panel, find the actual dimensions of the element you need to fill with an image, see which element sets the background for all its contents or simply learn about the structure of the UI. It is easy to use and modify.

Why should I care about this project?

On Windows Phone – rather than coding it yourself – the only other option for debugging visual trees I found was to pay for Silverlight Spy.

With the source code – you can easily add more details that you think might be missing or simply browse the Visual Studio “Watch” window to find them.

Full Source Code of VisualTreeDebugger for quick copy & paste

#if DEBUG
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace VisualTreeDebugger
{
    /// <summary>
    /// The class contains an BreakOnLoaded property that
    /// allows to debug a visual tree from the control
    /// it was applied to up to the visual tree root.
    /// </summary>
    /// <remarks>
    /// Just put a breakpoint in ControlLoaded()
    /// and set VisualTreeDebugger.BreakOnLoaded="True"
    /// on any FrameworkElement/Control.
    /// <para/>
    /// Set BreakOnLoaded if you want to put the breakpoint yourself.
    /// <para/>
    /// DebugVisualTree method has a local path variable
    /// that contains a list of all elements from the visual tree root
    /// up to the debugged control.
    /// <para/>
    /// Debug Output window contains common visual tree layout properties
    /// helpful in debugging layout issues.
    /// </remarks>
    public class VisualTreeDebugger
    {
        #region BreakOnLoaded
        /// <summary>
        /// BreakOnLoaded BreakOnLoaded Dependency Property
        /// </summary>
        public static readonly DependencyProperty BreakOnLoadedProperty =
            DependencyProperty.RegisterAttached(
            "BreakOnLoaded",
            typeof(bool),
            typeof(VisualTreeDebugger),
            new PropertyMetadata(false, OnBreakOnLoadedChanged));

        /// <summary>
        /// Gets the BreakOnLoaded property. This dependency property 
        /// indicates whether the debugger should BreakOnLoaded when control is loaded.
        /// </summary>
        public static bool GetBreakOnLoaded(DependencyObject d)
        {
            return (bool)d.GetValue(BreakOnLoadedProperty);
        }

        /// <summary>
        /// Sets the BreakOnLoaded property. This dependency property 
        /// indicates whether the debugger should BreakOnLoaded when control is loaded.
        /// </summary>
        public static void SetBreakOnLoaded(DependencyObject d, bool value)
        {
            d.SetValue(BreakOnLoadedProperty, value);
        }

        /// <summary>
        /// Handles changes to the BreakOnLoaded property.
        /// </summary>
        private static void OnBreakOnLoadedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if ((bool)e.NewValue)
            {
                ((FrameworkElement)d).Loaded += ControlLoaded;
            }
            else
            {
                ((FrameworkElement)d).Loaded -= ControlLoaded;
            }
        }
        #endregion

        #region BreakOnTap
        /// <summary>
        /// BreakOnTap Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty BreakOnTapProperty =
            DependencyProperty.RegisterAttached(
                "BreakOnTap",
                typeof(bool),
                typeof(VisualTreeDebugger),
                new PropertyMetadata(false, OnBreakOnTapChanged));

        /// <summary>
        /// Gets the BreakOnTap property. This dependency property 
        /// indicates whether the attached debugger should break when
        /// the FrameworkElement on which this property is set is tapped.
        /// </summary>
        public static bool GetBreakOnTap(DependencyObject d)
        {
            return (bool)d.GetValue(BreakOnTapProperty);
        }

        /// <summary>
        /// Sets the BreakOnTap property. This dependency property 
        /// indicates whether the attached debugger should break when
        /// the FrameworkElement on which this property is set is tapped.
        /// </summary>
        public static void SetBreakOnTap(DependencyObject d, bool value)
        {
            d.SetValue(BreakOnTapProperty, value);
        }

        /// <summary>
        /// Handles changes to the BreakOnTap property.
        /// </summary>
        private static void OnBreakOnTapChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var frameworkElement = d as FrameworkElement;

            Debug.Assert(frameworkElement != null, "BreakOnTapProperty should only be set on FrameworkElements.");

            if ((bool)e.NewValue)
            {
                ((FrameworkElement)d).Tap += ControlTapped;
            }
            else
            {
                ((FrameworkElement)d).Tap -= ControlTapped;
            }
        }
        #endregion

        /// <summary>
        /// Occurs when the control this behavior is BreakOnLoaded to gets loaded.
        /// Put a breakpoint in here and set ControlDebugOnLoaded.BreakOnLoaded="True"
        /// on any control to debug its visual tree.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private static void ControlLoaded(object sender, RoutedEventArgs e)
        {
            var startElement = (DependencyObject)sender;
            DebugVisualTree(startElement);
        }

        static void ControlTapped(object sender, GestureEventArgs e)
        {
            var startElement = (DependencyObject)sender;
            DebugVisualTree(startElement);
        }

        #region DebugVisualTree()
        public static void DebugVisualTree(DependencyObject startElement)
        {
            var path = new List<DependencyObject>();
            var dob = startElement;

            while (dob != null)
            {
                path.Add(dob);
                dob = VisualTreeHelper.GetParent(dob);
            }

            for (int i = path.Count - 1; i >= 0; i--)
            {
                TraceDependencyObject(path[i], i);
            }

            // Put breakpoint here
            Debug.WriteLine(
                string.Format("Watch path[0] to path[{0}]",
                path.Count - 1));

            if (Debugger.IsAttached)
            {
                Debugger.Break();
            }
        }
        #endregion

        #region TraceDependencyObject()
        private static void TraceDependencyObject(DependencyObject dob, int i)
        {
            var frameworkElement = dob as FrameworkElement;

            if (frameworkElement == null)
            {
                Debug.WriteLine(
                    "path[{0}] is Dependency Object: {1}",
                    i,
                    dob.GetType());
            }
            else
            {
                var c = frameworkElement as Control;
                var cc = frameworkElement as ContentControl;
                var panel = frameworkElement as Panel;
                var parentGrid = frameworkElement.Parent as Grid;

                Debug.WriteLine(
                    "path[{0}] is Control: {1}({2}):",
                    i,
                    frameworkElement.GetType(),
                    frameworkElement.Name ?? "<unnamed>");

                // Actual layout information
                Debug.WriteLine("\tActualWidth={0}\r\n\tActualHeight={1}", frameworkElement.ActualWidth, frameworkElement.ActualHeight);
                var pos = frameworkElement.TransformToVisual(Application.Current.RootVisual).Transform(new Point());
                var pos2 = frameworkElement.TransformToVisual(Application.Current.RootVisual).Transform(new Point(frameworkElement.ActualWidth, frameworkElement.ActualHeight));
                Debug.WriteLine("\tPosition - X={0}, Y={1}, Right={2}, Bottom={3}",
                    pos.X,
                    pos.Y,
                    pos2.X,
                    pos2.Y);

                // DataContext often turns out to be a surprise
                Debug.WriteLine("\tDataContext: {0} {1}", frameworkElement.DataContext, frameworkElement.DataContext != null ? "HashCode: " + frameworkElement.DataContext.GetHashCode() : "");

                // List common layout properties
                Debug.WriteLine("\tWidth={0}\r\n\tHeight={1}", frameworkElement.Width, frameworkElement.Height);
                Debug.WriteLine("\tHorizontalAlignment={0}\r\n\tVerticalAlignment={1}", frameworkElement.HorizontalAlignment, frameworkElement.VerticalAlignment);

                if (cc != null)
                {
                    Debug.WriteLine(
                        "\tHorizontalContentAlignment={0}\r\n\tVerticalContentAlignment={1}",
                        cc.HorizontalContentAlignment,
                        cc.VerticalContentAlignment);
                }

                Debug.WriteLine("\tMargins={0}", frameworkElement.Margin);

                if (cc != null)
                {
                    Debug.WriteLine("\tPadding={0}", cc.Padding);
                }

                if (panel != null)
                {
                    Debug.WriteLine("\tBackground={0}", panel.Background);
                }
                else if (c != null)
                {
                    Debug.WriteLine("\tBackground={0}", BrushToString(c.Background));
                    Debug.WriteLine("\tForeground={0}", BrushToString(c.Foreground));
                }

                if (parentGrid != null)
                {

                    var col = Grid.GetColumn(frameworkElement);
                    var row = Grid.GetRow(frameworkElement);

                    if (parentGrid.ColumnDefinitions.Count != 0 || col != 0)
                    {
                        Debug.Assert(
                            col < parentGrid.ColumnDefinitions.Count,
                            string.Format("Column {0} not defined on the parent Grid!", col));
                        Debug.WriteLine(
                            "\tColumn: {0} ({1})",
                            col,
                            parentGrid.ColumnDefinitions[col].Width);
                    }

                    if (parentGrid.RowDefinitions.Count != 0 || row != 0)
                    {
                        Debug.Assert(
                            row < parentGrid.RowDefinitions.Count,
                            string.Format("Row {0} not defined on the parent Grid!", row));
                        Debug.WriteLine(
                            "\tRow: {0} ({1})",
                            row,
                            parentGrid.RowDefinitions[col].Height);
                    }
                }

                if (frameworkElement.Parent is Canvas)
                {
                    var x = Canvas.GetLeft(frameworkElement);
                    var y = Canvas.GetTop(frameworkElement);
                    var zIndex = Canvas.GetZIndex(frameworkElement);

                    Debug.WriteLine("\tCanvas - X={0}, Y={1}, ZIndex={2}", x, y, zIndex);
                }
            }
        }
        #endregion

        #region BrushToString()
        private static string BrushToString(Brush brush)
        {
            if (brush == null)
                return "";

            var solidColorBrush = brush as SolidColorBrush;

            if (solidColorBrush != null)
            {
                return string.Format("SolidColorBrush: {0}", solidColorBrush.Color);
            }

            var imageBrush = brush as ImageBrush;

            if (imageBrush != null)
            {
                var bi = imageBrush.ImageSource as BitmapImage;

                if (bi != null)
                {
                    return string.Format(
                        "ImageBrush: {0}, UriSource: {1}",
                        bi,
                        bi.UriSource);
                }

                return string.Format("ImageBrush: {0}", imageBrush.ImageSource);
            }

            return brush.ToString();
        }
        #endregion
    }
}
#endif
Tagged , , , , ,