在WPF中,如何调试触发器?

38

在WPF中,有哪些好的方法可以调试这样的触发器?

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

理想情况下:

  • 如果触发器被触发,我希望在Visual Studio的Debug窗口中写入一条消息;
  • 如果触发器被触发,我希望Visual Studio在我的C#代码中触发一个断点。
1个回答

59

在WPF Mentor上,有一篇名为如何使用触发器跟踪进行调试的精彩文章(缓存版本在这里)。

我已经无数次使用它来调试触发器,在任何专业级别使用WPF的人看来,这是一种令人惊异的技术。

不幸的是,源代码链接部分损坏,因此如果原始文章消失,我会在SO上镜像。

更新:原始页面确实消失了 - 幸运的是我在SO上镜像了它!

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

代码:

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
}

7
非常感谢,真是太棒了!!!只是显而易见的是,在打印出“Enter”时,这意味着触发条件已解决为 True,“Exit”则表示触发条件已解决为 False。 - mattyb
1
@slugster 我同意,这是现在被接受的做法。这并不总是这样,几年前我如果不加免责声明就会被踩。 - Contango
5
别忘记添加 xmlns:my="clr-namespace:DebugTriggers" 命名空间。 - spacer
7
为了获得更有意义的信息,请将TriggerTraceStoryboardType枚举中的EnterExit重命名为类似于TriggerCondition_TrueTriggerCondition_False的内容。请注意,翻译后不要改变原文的意思,只需使其通俗易懂即可。 - spacer
我无法输出任何跟踪信息。它是否也适用于 MultiDataTrigger.Conditions - RuudSieb
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接