如何为EventTrigger设置条件?

22

在EventTrigger中是否可以设置条件?我编写了下面的EventTrigger(Mouse.MouseLeave)来触发单选按钮,但我希望它不会触发已选中项(IsChecked=True)。

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

请告诉我如何实现这个?

提前感谢。

6个回答

25

你不能以这种方式使用EventTrigger。WPF的RoutedEventHandler调用EventTriggers时不提供任何使触发器有条件的机制,并且你不能通过子类化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> {} 

享受吧!


2
你想要实际运行的代码吗?那需要额外支付10美元;-) 我之前写的只是脑海中的想法,有一些错别字和错误。我刚才把它放到Visual Studio中,修正了错别字并进行了测试。我已经更新了答案,提供了可运行的代码。 - Ray Burns
1
你是什么意思?我已经在5月12日凭借自己的记忆回答了你的问题。你在5月17日要求实际可用的代码,然后我在几个小时后发布了可用的代码。我知道我在5月17日发布的代码是可用的,因为我亲自测试过。请尝试一下。如果出现错误,请告诉我具体是哪些错误。 - Ray Burns
非常感谢您的回复。我尝试自己实现条件事件触发器,但基本上是做不到的。我想找到那些决定将所有触发器设为“密封内部”的WPF编码人员,并批评他们 :-(无论如何,当您尝试(并失败)使用一堆方法来实现条件事件触发器时,您最终几乎会得到与此处完全相同的解决方案。我添加的一个小修改是仅在条件更改时添加和删除虚拟触发器,而不是每次事件触发时都这样做。 - Orion Edwards
由于某些原因,这在控件上直接起作用,但在控件模板上却不起作用。对此有什么想法吗? - Wouter
这个解决方案看起来不错,但我不确定在我的XAML文件中应该在哪里使用条件触发器。如果原始的EventTrigger的源是一个矩形,那么我应该将ConditionalEventTrigger放在我的Rectangle元素内吗?此外,我遇到了编译错误,它说:“成员“Triggers”无法识别或无法访问。”,有关此问题的任何帮助? - Nadavrbn
显示剩余7条评论

11

这是对我有效的方法...

我想基于鼠标悬停在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>

4

这是我修改过的Ray的答案,它只在设置触发源时创建和附加虚拟事件,而不是每次都这样做。我认为这对我的情况更好,因为我要在数百个项目上引发事件,而不仅仅是一个或两个:

[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集合未被修改。这两个问题都可以通过额外的代码来解决。 - Ray Burns
为了允许单个对象上使用多个ConditionalEventTriggers,需添加以下代码:维护一个静态的RoutedEvent对象池。每次构建虚拟触发器时,从池中选择一个在该对象上任何其他EventTrigger中都没有使用的事件。如果不存在这样的事件,则注册一个新事件并将其添加到池中。 - Ray Burns
允许修改Actions集合的附加代码:将其实现为ObservableCollection。在ConditionalEventTrigger对象中存储对dummyTrigger的引用。当Action集合通知属性更改时,清除并重新填充dummyTrigger中的操作列表。 - Ray Burns

3
我知道这是一个旧帖子,但当我在这里寻找答案时,我发现以下内容对我有用。基本上,我想要一个面板,在鼠标悬停时从屏幕右侧动画显示,然后在鼠标离开时返回。但是,仅当面板未被固定时才会发生动画。在我的ViewModel中,存在IsShoppingCartPinned属性。至于您的情况,您可以将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>

0
基于 Ray 和 Orion,这是我的版本,目标是让您可以将 2 个触发器绑定到一个按钮,并在单击时翻转状态(或更多状态,如果您喜欢,并且它应该适用于所有控件)。当您绑定 ConditionProperty 时,有点棘手的是,您必须将 ConditionValue 写为 False 表示 True,True 表示 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> {}
}

我终于意识到,如果你使用一个按钮来翻转状态,然后更新UI,你不需要点击事件来改变UI,只需让状态改变UI即可,这意味着使用内置的DataTrigger将会得到相同的结果。唯一的区别在于概念上是事件驱动还是数据驱动,没有实际意义。所以,让我们忘记这个愚蠢的ConditionalEventTrigger吧。 - Hoyt_Ren
哦,不好。我们仍然需要这个。如果您要对多个对象进行动画处理,则可以使用此方法https://dev59.com/SnVD5IYBdhLWcg3wHn2d,但是此方法仅适用于模板中的所有对象。如果您想要对另一个控件进行动画处理,则仍需要EventTrigger,这是由于微软的错误所致。 - Hoyt_Ren

0

在你的情况下,你需要:

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

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

   <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>

谢谢回复。我的实际问题是,当鼠标离开控件时,所有元素都应该发生动画效果。但在选中状态下不应发生动画效果。 - Prabu
我更新了我的初始答案。你需要使用多数据触发器并定义你的附加条件。 - J Rothe
1
MultiDataTrigger的问题在于任何一个条件的变化都会导致动画的播放。例如,在这种情况下,如果用户的鼠标在UI的其他位置,并且他们更改了数据以使复选框被选中,则即使鼠标不在附近,动画也会播放。(还要注意您在MultiDataTrigger中省略了第二个条件,我假设它应该是<Condition SourceName="checkbox" Proerty="IsChecked" Value="True" />) - Ray Burns
我忘记了第二个条件 - 我期望任何其他条件都会根据需要填写,因为这只是一个起点。话虽如此,我认为你的条件是相反的 - 他想要动画播放的是未选中复选框的任何内容,而你的条件正在执行如果为真。也许添加一个附加的依赖属性来允许反转条件?我想他可以使用ValueConverter,但对于简单的布尔运算来说,这似乎是多余的... - J Rothe

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