【问题标题】:How to give the condition for EventTrigger?如何给出EventTrigger的条件?
【发布时间】:2021-02-23 23:53:12
【问题描述】:

是否可以在 EventTrigger 中给出条件?我为单选按钮编写了以下 EventTrigger (Mouse.MouseLeave)。我希望不应该为处于选中状态(IsChecked = True)的项目触发此操作。

<EventTrigger RoutedEvent="Mouse.MouseLeave" SourceName="border">                                 
      <BeginStoryboard Name="out_BeginStoryboard" Storyboard="{StaticResource out}" />
      <RemoveStoryboard BeginStoryboardName="over_BeginStoryboard" />
</EventTrigger>

请告诉我如何实现这一目标?

提前致谢。

【问题讨论】:

    标签: wpf


    【解决方案1】:

    您不能以这种方式使用 EventTrigger。调用 EventTriggers 的 WPF 的 RoutedEventHandler 没有提供任何使触发器有条件的机制,并且您无法通过子类化 TriggerAction 来解决此问题,因为没有要覆盖的受保护的 Invoke() 或 Execute() 操作。

    然而,这可以很容易地使用自定义类来完成。以下是它的使用方法:

    <Border>
      <my:ConditionalEventTrigger.Triggers>
        <my:ConditionalEventTriggerCollection>
          <my:ConditionalEventTrigger RoutedEvent="Mouse.MouseLeave"
                                      Condition="{Binding IsChecked, ElementName=checkbox}">
            <BeginStoryboard Name="out_BeginStoryboard" Storyboard="{StaticResource out}" />               
            <RemoveStoryboard BeginStoryboardName="over_BeginStoryboard" />               
          </my:ConditionalEventTrigger>               
        </my:ConditionalEventTriggerCollection>
      </my:ConditionalEventTrigger.Triggers>
      ...
    

    以下是它的实现方式:

    [ContentProperty("Actions")] 
    public class ConditionalEventTrigger : FrameworkContentElement
    { 
      public RoutedEvent RoutedEvent { get; set; } 
      public List<TriggerAction> Actions { get; set; }
    
      // Condition
      public bool Condition { get { return (bool)GetValue(ConditionProperty); } set { SetValue(ConditionProperty, value); } }
      public static readonly DependencyProperty ConditionProperty = DependencyProperty.Register("Condition", typeof(bool), typeof(ConditionalEventTrigger));
    
      // "Triggers" attached property
      public static ConditionalEventTriggerCollection GetTriggers(DependencyObject obj) { return (ConditionalEventTriggerCollection)obj.GetValue(TriggersProperty); }
      public static void SetTriggers(DependencyObject obj, ConditionalEventTriggerCollection value) { obj.SetValue(TriggersProperty, value); }
      public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached("Triggers", typeof(ConditionalEventTriggerCollection), typeof(ConditionalEventTrigger), new PropertyMetadata 
      { 
        PropertyChangedCallback = (obj, e) => 
        { 
          // When "Triggers" is set, register handlers for each trigger in the list 
          var element = (FrameworkElement)obj; 
          var triggers = (List<ConditionalEventTrigger>)e.NewValue;
          foreach(var trigger in triggers)
            element.AddHandler(trigger.RoutedEvent, new RoutedEventHandler((obj2, e2) =>
              trigger.OnRoutedEvent(element)));
        } 
      });
    
      public ConditionalEventTrigger()
      {
        Actions = new List<TriggerAction>();
      }
    
      // When an event fires, check the condition and if it is true fire the actions 
      void OnRoutedEvent(FrameworkElement element) 
      { 
        DataContext = element.DataContext;  // Allow data binding to access element properties
        if(Condition) 
        { 
          // Construct an EventTrigger containing the actions, then trigger it 
          var dummyTrigger = new EventTrigger { RoutedEvent = _triggerActionsEvent }; 
          foreach(var action in Actions) 
            dummyTrigger.Actions.Add(action); 
    
          element.Triggers.Add(dummyTrigger); 
          try 
          { 
            element.RaiseEvent(new RoutedEventArgs(_triggerActionsEvent)); 
          } 
          finally 
          { 
            element.Triggers.Remove(dummyTrigger); 
          } 
        } 
      } 
    
      static RoutedEvent _triggerActionsEvent = EventManager.RegisterRoutedEvent("", RoutingStrategy.Direct, typeof(EventHandler), typeof(ConditionalEventTrigger)); 
    
    } 
    
    // Create collection type visible to XAML - since it is attached we cannot construct it in code 
    public class ConditionalEventTriggerCollection : List<ConditionalEventTrigger> {} 
    

    享受吧!

    【讨论】:

    • 你能在这里移植一个可行的代码吗?您发布的内容不起作用。如果您为此发布任何示例应用程序,将会有很大帮助。
    • 什么,你想要实际运行的代码?那将是额外的 10 美元;-) 我之前写的只是在我的脑海中,并且有一些错别字和错误。我刚才把它放到了 Visual Studio 中,修正了拼写错误,并进行了测试。我已经用工作代码更新了答案。
    • 什么意思?我做到了。我在 5 月 12 日突然回答了你的问题。您在 5 月 17 日请求了实际的工作代码,几个小时后我发布了工作代码。我知道我在 5 月 17 日发布的代码有效,因为我实际测试过它。请尝试一下。如果您遇到错误,请告诉我它们是什么。
    • 非常感谢您的回复。我尝试自己实现条件事件触发器,而你基本上做不到。我想找到决定制作所有触发器的 WPF 编码人员sealed internal 并拍打他们 :-( 无论如何,当你尝试(并失败)用一堆方法来实现条件事件触发器时,你几乎结束了与您在此处获得的确切解决方案一致。我添加的一个小调整是,它仅在条件更改时添加和删除虚拟触发器,而不是每次触发事件时。
    • @Orion:你描述的“调整”对我来说没有意义。 EventTriggers 几乎不会每秒触发超过 1 或 2 次,但一个条件可以很容易地每秒更改数千次。所以一般用途最安全的做法是在事件触发期间完成工作。我的方式也需要更少的代码,原因有两个:1.按照您建议的方式,您必须对 Actions 使用 ObservableCollection 并处理 INotifyCollectionChanged 来更新虚拟触发器,这是很多代码,以及 2.您需要跟踪虚拟触发器,以便稍后将其删除。
    【解决方案2】:

    这对我有用...

    我想根据鼠标悬停在 UI 元素上并且 UI 元素的关联所有者处于活动状态(即启用以使玩家移动)来执行动画。

    为了支持这些要求,我使用了相对源绑定来克服对事件触发条件的支持不足。

    例子:

    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding RelativeSource={RelativeSource self}, Path=IsMouseOver}" Value="True" />
            <Condition Binding="{Binding Path=IsPlayer1Active}" Value="True" />
        </MultiDataTrigger.Conditions>
        <MultiDataTrigger.EnterActions>
            <BeginStoryboard>
                <Storyboard>
                    <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[0].Color" To="#FF585454" Duration="0:0:.25"/>
                    <ColorAnimation Storyboard.TargetProperty="Background.GradientStops[1].Color" To="Black" Duration="0:0:2"/>
                </Storyboard>
            </BeginStoryboard>
        </MultiDataTrigger.EnterActions>
    </MultiDataTrigger>
    

    【讨论】:

      【解决方案3】:

      这是我修改后的Ray's answer 版本,它仅在设置源触发器时创建和附加虚拟事件,而不是每次都这样做。我认为这对我的场景会更好,因为我要针对数百个项目发起事件,而不仅仅是一两个:

      [ContentProperty("Actions")]
      public class ConditionalEventTrigger : FrameworkContentElement
      {
          static readonly RoutedEvent DummyEvent = EventManager.RegisterRoutedEvent(
              "", RoutingStrategy.Direct, typeof(EventHandler), typeof(ConditionalEventTrigger));
      
          public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
              "Triggers", typeof(ConditionalEventTriggers), typeof(ConditionalEventTrigger),
              new FrameworkPropertyMetadata(RefreshTriggers));
      
          public static readonly DependencyProperty ConditionProperty = DependencyProperty.Register(
              "Condition", typeof(bool), typeof(ConditionalEventTrigger)); // the Condition is evaluated whenever an event fires
      
          public ConditionalEventTrigger()
          {
              Actions = new List<TriggerAction>();
          }
      
          public static ConditionalEventTriggers GetTriggers(DependencyObject obj)
          { return (ConditionalEventTriggers)obj.GetValue(TriggersProperty); }
      
          public static void SetTriggers(DependencyObject obj, ConditionalEventTriggers value)
          { obj.SetValue(TriggersProperty, value); }
      
          public bool Condition
          {
              get { return (bool)GetValue(ConditionProperty); }
              set { SetValue(ConditionProperty, value); }
          }
      
          public RoutedEvent RoutedEvent { get; set; }
          public List<TriggerAction> Actions { get; set; }
      
          // --- impl ----
      
          // we can't actually fire triggers because WPF won't let us (stupid sealed internal methods)
          // so, for each trigger, make a dummy trigger (on a dummy event) with the same actions as the real trigger,
          // then attach handlers for the dummy event
          public static void RefreshTriggers(DependencyObject obj, DependencyPropertyChangedEventArgs e)
          {
              var targetObj = (FrameworkElement)obj;
              // start by clearing away the old triggers
              foreach (var t in targetObj.Triggers.OfType<DummyEventTrigger>().ToArray())
                  targetObj.Triggers.Remove(t);
      
              // create and add dummy triggers
              foreach (var t in ConditionalEventTrigger.GetTriggers(targetObj))
              {
                  t.DataContext = targetObj.DataContext; // set and Track DataContext so binding works
                  // targetObj.GetDataContextChanged().WeakSubscribe(dc => t.DataContext = targetObj.DataContext);
      
                  var dummyTrigger = new DummyEventTrigger { RoutedEvent = DummyEvent };
                  foreach (var action in t.Actions)
                      dummyTrigger.Actions.Add(action);
      
                  targetObj.Triggers.Add(dummyTrigger);
                  targetObj.AddHandler(t.RoutedEvent, new RoutedEventHandler((o, args) => {
                      if (t.Condition) // evaluate condition when the event gets fired
                          targetObj.RaiseEvent(new RoutedEventArgs(DummyEvent));
                  }));
              }
          }
      
          class DummyEventTrigger : EventTrigger { }
      }
      
      public class ConditionalEventTriggers : List<ConditionalEventTrigger> { }
      

      它是这样使用的:

      <Border>
        <local:ConditionalEventTrigger.Triggers>
          <local:ConditionalEventTriggers>
            <local:ConditionalEventTrigger RoutedEvent="local:ClientEvents.Flash" Condition="{Binding IsFlashing}">
              <BeginStoryboard Name="FlashAnimation">...
      

      线

      // targetObj.GetDataContextChanged().WeakSubscribe(dc => t.DataContext = targetObj.DataContext);
      

      正在使用我写的响应式框架和一些扩展方法,基本上我们需要订阅目标对象的.DataContextChanged事件,但是我们需要使用弱引用来完成。如果您的对象从未更改其数据上下文,那么您根本不需要此代码

      【讨论】:

      • 这种修改效率更高,但请注意,它只能在以下情况下使用:1. 仅设置了一个 ConditionalEventTrigger,以及 2. Actions 集合在初始化后永远不会修改。这两个问题都可以通过额外的代码来解决。
      • 允许在单个对象上使用多个 ConditionalEventTrigger 的附加代码:维护静态 RoutedEvent 对象池。每次构造一个虚拟触发器时,从池中选择一个未在对象上的任何其他 EventTrigger 中使用的事件。如果不存在此类事件,请注册一个新事件并将其添加到池中。
      • 允许修改 Actions 集合的附加代码:将其实现为 ObservableCollection。在 ConditionalEventTrigger 对象中存储对 dummyTrigger 的引用。当 Action 集合通知属性更改时,清除并重新填充 dummyTrigger 中的操作列表。
      【解决方案4】:

      我知道这是一篇旧帖子,但是当我最终在这里寻求答案时,这对我有用。基本上我想要一个面板,它会在鼠标悬停时从屏幕右侧进行动画处理,然后在鼠标离开时返回。但是,只有当面板没有固定时。 IsShoppingCartPinned 属性存在于我的 ViewModel 上。就您的场景而言,您可以将 IsShoppingCartPinned 属性替换为您的复选框 IsChecked 属性,并在 EventTriggers 上运行任何类型的动画。

      代码如下:

      <Grid.Style>
           <Style TargetType="{x:Type Grid}">
                <Setter Property="Margin" Value="0,20,-400,20"/>
                <Setter Property="Grid.Column" Value="0"/>
                <Style.Triggers>
                     <MultiDataTrigger>
                          <MultiDataTrigger.Conditions>
                               <Condition Binding="{Binding IsShoppingCartPinned}" Value="False"/>
                               <Condition Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}" Value="True"/>
                          </MultiDataTrigger.Conditions>
                          <MultiDataTrigger.EnterActions>
                               <BeginStoryboard Name="ExpandPanel">
                                    <Storyboard>
                                         <ThicknessAnimation Duration="0:0:0.1" Storyboard.TargetProperty="Margin" To="0,20,0,20"/>
                                    </Storyboard>
                               </BeginStoryboard>
                          </MultiDataTrigger.EnterActions>
                          <MultiDataTrigger.ExitActions>
                               <BeginStoryboard Name="HidePanel">
                                    <Storyboard>
                                         <ThicknessAnimation Duration="0:0:0.1" Storyboard.TargetProperty="Margin" To="0,20,-400,20"/>
                                    </Storyboard>
                               </BeginStoryboard>
                          </MultiDataTrigger.ExitActions>
                     </MultiDataTrigger>
                     <DataTrigger Binding="{Binding IsShoppingCartPinned}" Value="True">
                          <DataTrigger.EnterActions>
                               <RemoveStoryboard BeginStoryboardName="ExpandPanel"/>
                               <RemoveStoryboard BeginStoryboardName="HidePanel"/>
                          </DataTrigger.EnterActions>
                          <DataTrigger.Setters>
                               <Setter Property="Margin" Value="0"/>
                               <Setter Property="Grid.Column" Value="1"/>
                          </DataTrigger.Setters>
                     </DataTrigger>
                </Style.Triggers>
           </Style>
      </Grid.Style>
      

      【讨论】:

        【解决方案5】:

        根据您的情况,您需要:

        <EventTrigger RoutedEvent="Checked" SourceName="border">
        

        编辑: 根据您的 cmets,您正在寻找一个多数据触发器。

           <MultiDataTrigger>
                <MultiDataTrigger.Conditions>
                    <Condition SourceName="border" Property="IsMouseOver" Value="false" />                                            
                </MultiDataTrigger.Conditions>
                <MultiDataTrigger.EnterActions>
                    <BeginStoryboard Name="out_BeginStoryboard" Storyboard="{StaticResource out}" />
                    <RemoveStoryboard BeginStoryboardName="over_BeginStoryboard" />
                </MultiDataTrigger.EnterActions>
           </MultiDataTrigger>
        

        【讨论】:

        • 感谢您的回复。我的实际查询是,当鼠标离开控件时,所有元素都应该发生动画。但它不应该在处于 Checed 状态时发生。
        • 我更新了我的初始答案。您需要使用多数据触发器并定义其他条件。
        • MultiDataTrigger 的问题在于任一条件变化都可能导致动画。例如,在这种情况下,如果用户的鼠标位于 UI 中的其他位置,并且他们更改数据导致复选框被选中,则即使鼠标不在附近,动画也会播放。 (另请注意,您省略了 MultiDataTrigger 中的第二个条件,我假设它是 &lt;Condition SourceName="checkbox" Proerty="IsChecked" Value="True" /&gt;
        • 是的,我省略了第二个条件 - 我希望根据需要填写任何其他条件,因为它只是一个起点。也就是说,我认为您的条件是相反的-他希望动画播放任何未选中复选框并且您的条件为真时执行的任何内容。也许添加一个额外的依赖属性以允许反转条件?我想他可以使用 ValueConverter 但这对于简单的 bool 操作来说似乎无关紧要......
        【解决方案6】:

        基于 Ray 和 Orion,这是我的版本,目标是您可以将 2 个触发器绑定到一个按钮,并在单击时翻转状态(或更多状态,如果您愿意,它应该适用于所有 Control)。当您绑定 ConditionProperty 时,您必须将 ConditionValue 写为 False 表示 True,将 Ture 表示为 False,这有点棘手。我猜这是因为在更新绑定之前执行了按钮的事件处理程序。它是这样使用的:

        <Button x:Name="HoldButton" Content="{Binding Status.Running}"/>
            <mut:ConditionalEventTrigger.ConditionTriggers>
                <mut:ConditionalEventTriggers>
                    <mut:ConditionalEventTrigger RoutedEvent="ButtonBase.Click" ConditionProperty="{Binding Status.Running}" ConditionValue="False">
                        <BeginStoryboard x:Name="OnHold_BeginStoryboard" Storyboard="{StaticResource OnHold}"/>
                    </mut:ConditionalEventTrigger>
                    <mut:ConditionalEventTrigger RoutedEvent="ButtonBase.Click" ConditionProperty="{Binding Status.Running}" ConditionValue="True">
                        <StopStoryboard BeginStoryboardName="OnHold_BeginStoryboard"/>
                    </mut:ConditionalEventTrigger>
                </mut:ConditionalEventTriggers>
            </mut:ConditionalEventTrigger.ConditionTriggers>
        </Button>
        

        代码如下:

        using System;
        using System.Collections.Generic;
        using System.Windows;
        using System.Windows.Markup;
        
        namespace MyUtility.Trigger
        {
            [ContentProperty("Actions")]
            public class ConditionalEventTrigger : FrameworkContentElement
            {
                public static readonly DependencyProperty ConditionTriggersProperty = DependencyProperty.RegisterAttached(
                    "ConditionTriggers",
                    typeof(ConditionalEventTriggers),
                    typeof(ConditionalEventTrigger),
                    new FrameworkPropertyMetadata(OnConditionalEventTriggersChanged));
        
                public static ConditionalEventTriggers GetConditionTriggers(FrameworkElement element)
                {
                    return (ConditionalEventTriggers)element.GetValue(ConditionTriggersProperty);
                }
        
                public static void SetConditionTriggers(FrameworkElement element, List<ConditionalEventTrigger> value)
                {
                    element.SetValue(ConditionTriggersProperty, value);
                }
        
                public static readonly DependencyProperty ConditionPropertyProperty = DependencyProperty.Register(
                    "ConditionProperty",
                    typeof(bool),
                    typeof(ConditionalEventTrigger));
        
                public bool ConditionProperty
                {
                    get
                    {
                        return (bool)GetValue(ConditionPropertyProperty);
                    }
                    set
                    {
                        SetValue(ConditionPropertyProperty, value);
                    }
                }
        
                public static readonly DependencyProperty ConditionValueProperty = DependencyProperty.Register(
                    "ConditionValue",
                    typeof(bool),
                    typeof(ConditionalEventTrigger));
        
                public bool ConditionValue
                {
                    get
                    {
                        return (bool)GetValue(ConditionValueProperty);
                    }
                    set
                    {
                        SetValue(ConditionValueProperty, value);
                    }
                }
        
                private static readonly RoutedEvent m_DummyEvent = EventManager.RegisterRoutedEvent(
                    "ConditionalEventTriggerDummyEvent",
                    RoutingStrategy.Direct,
                    typeof(EventHandler),
                    typeof(ConditionalEventTrigger));
        
                public RoutedEvent RoutedEvent { get; set; }
                public List<TriggerAction> Actions { get; set; }
        
                public ConditionalEventTrigger()
                {
                    Actions = new List<TriggerAction>();
                }
        
                public static void OnConditionalEventTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
                {
                    var element = (FrameworkElement)obj;
                    var triggers = (ConditionalEventTriggers)e.NewValue;
                    foreach(ConditionalEventTrigger t in triggers)
                    {
                        element.RemoveHandler(t.RoutedEvent, new RoutedEventHandler((obj2, e2) => t.OnRoutedEvent(element)));
                        element.AddHandler(t.RoutedEvent, new RoutedEventHandler((obj2, e2) => t.OnRoutedEvent(element)));
                    }
                }
        
                public void OnRoutedEvent(FrameworkElement element)
                {
                    this.DataContext = element.DataContext;
                    if (this.ConditionProperty == this.ConditionValue)
                    {
                        // .Net doesn't allow us fire a trigger directly, so we bingd trigger on Element and then fire the element.
                        var dummyTrigger = new EventTrigger { RoutedEvent = m_DummyEvent };
        
                        foreach (TriggerAction action in this.Actions)
                        {
                            dummyTrigger.Actions.Add(action);
                        }
        
                        element.Triggers.Add(dummyTrigger);
        
                        try
                        {
                            element.RaiseEvent(new RoutedEventArgs(m_DummyEvent));
                        }
                        finally
                        {
                            element.Triggers.Remove(dummyTrigger);
                        }
                    }
                }
            }
        
            public class ConditionalEventTriggers : List<ConditionalEventTrigger> {}
        }
        

        【讨论】:

        • 我终于意识到,如果你用一个按钮来flp一个状态,然后更新UI,你不需要点击事件来改变UI,简单的让状态改变UI就可以了好的,这意味着使用内置的 DataTrigger 你会得到相同的结果。唯一不同的是概念上的事件驱动或数据驱动,根本没有真正的意义。所以,让我们忘记这个愚蠢的 ConditionalEventTrigger。
        • 哦,不。我们仍然需要这个。如果您为多个对象设置动画,您可以这样做stackoverflow.com/questions/80388/…,但这仅适用于模板中的所有对象。如果您想为另一个控件设置动画,由于 MS 的故障,您仍然需要 EventTrigger。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-08
        • 1970-01-01
        • 2022-11-04
        • 2010-12-13
        • 2022-07-20
        • 1970-01-01
        相关资源
        最近更新 更多