In WPF, how to debug triggers?
Asked Answered
S

1

39

In WPF, what are some good approaches to debug a trigger such as this?

<Trigger Property="IsMouseOver" Value="True">  
   <Setter Property="FontWeight" Value="Bold"/>  
</Trigger>

Ideally:

  • If the trigger has been hit, I would like a message to be written to the Debug window within Visual Studio;
  • If the trigger is hit, I want Visual Studio to hit a breakpoint in my C# code.
Samsara answered 28/8, 2015 at 16:12 Comment(0)
S
61

There is an excellent article on WPF Mentor by entitled How to debug triggers using Trigger-Tracing (cached version here).

I've used it innumerable times to debug into triggers, it's an amazing technique for anybody that uses WPF at a professional level.

Unfortunately, the link to the source code is partially broken, so I am mirroring this on SO in case the original article disappears.

Update: the original page did disappear - lucky I mirrored it!

Debugging triggers is a painful process: they work behind the scenes, there's nowhere to put a breakpoint and no call-stack to help you. The usual approach taken is trial and error based and it nearly always takes longer than it should to work out what's going wrong.

This post describes a new technique for debugging triggers allowing you to log all trigger actions along with the elements being acted upon:

enter image description here

It's good because it:

  • helps you fix all manner of problems :)
  • works on all types of trigger: Trigger, DataTrigger, MultiTrigger etc.
  • allows you to add breakpoints when any trigger is entered and/or exited
  • is easy to set up: just drop one source file (TriggerTracing.cs) into your app and set these attached properties to the trigger to be traced:

    <Trigger my:TriggerTracing.TriggerName="BoldWhenMouseIsOver"  
         my:TriggerTracing.TraceEnabled="True"  
         Property="IsMouseOver" Value="True">  
        <Setter Property="FontWeight" Value="Bold"/>  
    </Trigger> 
    

    and also add the my namespace with xmlns:my="clr-namespace:DebugTriggers".

It works by:

  • using attached properties to add dummy animation storyboards to the trigger
  • activating WPF animation tracing and filtering the results to only the entries with the dummy storyboards

Code:

using System.Diagnostics;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Media.Animation;

// Code from http://www.wpfmentor.com/2009/01/how-to-debug-triggers-using-trigger.html
// No license specified - this code is trimmed out from Release build anyway so it should be ok using it this way

// HOWTO: add the following attached property to any trigger and you will see when it is activated/deactivated in the output window
//        TriggerTracing.TriggerName="your debug name"
//        TriggerTracing.TraceEnabled="True"

// Example:
// <Trigger my:TriggerTracing.TriggerName="BoldWhenMouseIsOver"  
//          my:TriggerTracing.TraceEnabled="True"  
//          Property="IsMouseOver"  
//          Value="True">  
//     <Setter Property = "FontWeight" Value="Bold"/>  
// </Trigger> 
//
// As this works on anything that inherits from TriggerBase, it will also work on <MultiTrigger>.

namespace DebugTriggers
{
#if DEBUG

    /// <summary>
    /// Contains attached properties to activate Trigger Tracing on the specified Triggers.
    /// This file alone should be dropped into your app.
    /// </summary>
    public static class TriggerTracing
    {
        static TriggerTracing()
        {
            // Initialise WPF Animation tracing and add a TriggerTraceListener
            PresentationTraceSources.Refresh();
            PresentationTraceSources.AnimationSource.Listeners.Clear();
            PresentationTraceSources.AnimationSource.Listeners.Add(new TriggerTraceListener());
            PresentationTraceSources.AnimationSource.Switch.Level = SourceLevels.All;
        }

        #region TriggerName attached property

        /// <summary>
        /// Gets the trigger name for the specified trigger. This will be used
        /// to identify the trigger in the debug output.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <returns></returns>
        public static string GetTriggerName(TriggerBase trigger)
        {
            return (string)trigger.GetValue(TriggerNameProperty);
        }

        /// <summary>
        /// Sets the trigger name for the specified trigger. This will be used
        /// to identify the trigger in the debug output.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <returns></returns>
        public static void SetTriggerName(TriggerBase trigger, string value)
        {
            trigger.SetValue(TriggerNameProperty, value);
        }

        public static readonly DependencyProperty TriggerNameProperty =
            DependencyProperty.RegisterAttached(
            "TriggerName",
            typeof(string),
            typeof(TriggerTracing),
            new UIPropertyMetadata(string.Empty));

        #endregion

        #region TraceEnabled attached property

        /// <summary>
        /// Gets a value indication whether trace is enabled for the specified trigger.
        /// </summary>
        /// <param name="trigger">The trigger.</param>
        /// <returns></returns>
        public static bool GetTraceEnabled(TriggerBase trigger)
        {
            return (bool)trigger.GetValue(TraceEnabledProperty);
        }

        /// <summary>
        /// Sets a value specifying whether trace is enabled for the specified trigger
        /// </summary>
        /// <param name="trigger"></param>
        /// <param name="value"></param>
        public static void SetTraceEnabled(TriggerBase trigger, bool value)
        {
            trigger.SetValue(TraceEnabledProperty, value);
        }

        public static readonly DependencyProperty TraceEnabledProperty =
            DependencyProperty.RegisterAttached(
            "TraceEnabled",
            typeof(bool),
            typeof(TriggerTracing),
            new UIPropertyMetadata(false, OnTraceEnabledChanged));

        private static void OnTraceEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var triggerBase = d as TriggerBase;

            if (triggerBase == null)
                return;

            if (!(e.NewValue is bool))
                return;

            if ((bool)e.NewValue)
            {
                // insert dummy story-boards which can later be traced using WPF animation tracing

                var storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Enter);
                triggerBase.EnterActions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });

                storyboard = new TriggerTraceStoryboard(triggerBase, TriggerTraceStoryboardType.Exit);
                triggerBase.ExitActions.Insert(0, new BeginStoryboard() { Storyboard = storyboard });
            }
            else
            {
                // remove the dummy storyboards

                foreach (TriggerActionCollection actionCollection in new[] { triggerBase.EnterActions, triggerBase.ExitActions })
                {
                    foreach (TriggerAction triggerAction in actionCollection)
                    {
                        BeginStoryboard bsb = triggerAction as BeginStoryboard;

                        if (bsb != null && bsb.Storyboard != null && bsb.Storyboard is TriggerTraceStoryboard)
                        {
                            actionCollection.Remove(bsb);
                            break;
                        }
                    }
                }
            }
        }

        #endregion

        private enum TriggerTraceStoryboardType
        {
            Enter, Exit
        }

        /// <summary>
        /// A dummy storyboard for tracing purposes
        /// </summary>
        private class TriggerTraceStoryboard : Storyboard
        {
            public TriggerTraceStoryboardType StoryboardType { get; private set; }
            public TriggerBase TriggerBase { get; private set; }

            public TriggerTraceStoryboard(TriggerBase triggerBase, TriggerTraceStoryboardType storyboardType)
            {
                TriggerBase = triggerBase;
                StoryboardType = storyboardType;
            }
        }

        /// <summary>
        /// A custom tracelistener.
        /// </summary>
        private class TriggerTraceListener : TraceListener
        {
            public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string format, params object[] args)
            {
                base.TraceEvent(eventCache, source, eventType, id, format, args);

                if (format.StartsWith("Storyboard has begun;"))
                {
                    TriggerTraceStoryboard storyboard = args[1] as TriggerTraceStoryboard;
                    if (storyboard != null)
                    {
                        // add a breakpoint here to see when your trigger has been
                        // entered or exited

                        // the element being acted upon
                        object targetElement = args[5];

                        // the namescope of the element being acted upon
                        INameScope namescope = (INameScope)args[7];

                        TriggerBase triggerBase = storyboard.TriggerBase;
                        string triggerName = GetTriggerName(storyboard.TriggerBase);

                        Debug.WriteLine(string.Format("Element: {0}, {1}: {2}: {3}",
                            targetElement,
                            triggerBase.GetType().Name,
                            triggerName,
                            storyboard.StoryboardType));
                    }
                }
            }

            public override void Write(string message)
            {
            }

            public override void WriteLine(string message)
            {
            }
        }
    }
#endif
}
Samsara answered 28/8, 2015 at 16:12 Comment(7)
THANK YOU SO MUCH, brilliant!!!! Just to point out the obvious, in case it's not immediately clear to anyone, when it prints "Enter" that means the trigger condition resolved to True and "Exit" means the trigger condition resolved to False.Schroth
@Midas Glad to help, and thanks for the clarification!Samsara
I removed the note at the top of your question and answer - there was no need for it as self answers are absolutely fine (you don't need any sort of note/disclaimer).Oculo
@Oculo I agree, this is accepted practice now. This wasn't always the case, a couple of years ago I used to get downvotes unless I put this disclaimer.Samsara
Don't forget to add the "my" namespace with xmlns:my="clr-namespace:DebugTriggers".Brote
To get a more meaningful info, rename the Enter and Exit in the TriggerTraceStoryboardType enum to something like TriggerCondition_True and TriggerCondition_False.Brote
I cannot get it to output any traces. Does it also work with MultiDataTrigger.Conditions?Aeschines

© 2022 - 2024 — McMap. All rights reserved.