22

Is it possible to give the condition within the EventTrigger?? I have written a following EventTrigger (Mouse.MouseLeave) for Radio button. I want this should not be trigged for an item which is in Checked condition (IsChecked=True).

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

Please let me know how can I achieve this?

Thanks in advance.

Prabu
  • 229
  • 1
  • 2
  • 4

6 Answers6

25

You can't use EventTrigger in this way. WPF's RoutedEventHandler that invokes EventTriggers doesn't provide any mechanism for making the trigger conditional, and you can't fix this by subclassing TriggerAction because there is no protected Invoke() or Execute() action to override.

However this can be done quite easily using a custom class. Here's how it would be used:

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

And here's how it would be implemented:

[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> {} 

Enjoy!

Ray Burns
  • 62,163
  • 12
  • 140
  • 141
  • Could you please port a workable code here? The which you posted is not working. It would be of great help if you post any sample application for this. – Prabu May 17 '10 at 07:15
  • 2
    What, you want actual functioning code? That will be an extra $10 please ;-) What I wrote before was just off the top of my head and had some typos and a bug. I dropped it into Visual Studio just now, fixed the typos, and tested it. I've updated the answer with the working code. – Ray Burns May 17 '10 at 16:29
  • 1
    What do you mean? I did. I answered your question on May 12 off the top of my head. You requested actual working code on May 17, and I posted working code a few hours later. I know the code I posted on May 17 works because I actually tested it. Please try it. If you get errors, please let me know what they are. – Ray Burns May 31 '10 at 03:21
  • Thanks a lot for this response. I tried myself to implement Conditional event triggers, and you basically just can't do it. I want to find the WPF coders who decided to make all the trigger stuff `sealed internal` and slap them :-( Anyway, by the time you try (and fail) with a bunch of approaches to implement Conditional event triggers, you pretty much end up with the exact solution you've got here. One minor tweak I added is that it adds and removes the dummy triggers only when the condition changes, not every time an event fires. – Orion Edwards Aug 01 '10 at 23:51
  • @Orion: The "tweak" you describe doesn't make sense to me. EventTriggers are almost never fired more than a 1 or 2 times per second, but a condition can easily change thousands of times per second. So the safest course for general purpose use is to do the work during the event trigger. It also takes a lot less code my way, for two reasons: 1. With the way you suggest you have to use an ObservableCollection for Actions and handle INotifyCollectionChanged to update the dummy trigger, which is a lot of code, and 2. You need to keep track of the dummy trigger so you can remove it later. – Ray Burns Aug 04 '10 at 00:12
  • @Ray: I'm broadcasting an event. WPF doesn't do broadcasted events, so I basically walk the visual tree and raise a Direct event on each child element. While the 'source' only fires 1 or 2 times a second, it gets raised on hundreds of elements each time. Regarding the actions list, the list gets frozen by WPF and never modified, so there's no need for an ObservableCollection of actions or any of that associated code – Orion Edwards Aug 04 '10 at 20:38
  • @Ray: I've added my version [here](http://stackoverflow.com/questions/2764415/how-to-give-the-condition-for-eventtrigger/2823333#2823333). Hopefully my comments make more sense now – Orion Edwards Aug 04 '10 at 20:49
  • @Orion: Thanks for posting that. I had misunderstood how you were doing it. FYI, there is a bug in your code: If you register two conditional event handlers, all actions for both will fire for either event if the condition is true. This can be solved by creating a pool of RoutedEvents and allocating from it so each ConditionalEventHandler applied to an object gets a different RoutedEvent. No additional recordkeeping is required, as you can scan the existing handlers to find the next unused RoutedEvent. – Ray Burns Aug 04 '10 at 23:30
  • @Orion: Also, by the way, the Actions collection is not frozen by WPF since it is a `List<>` which cannot be frozen. – Ray Burns Aug 04 '10 at 23:32
  • @Ray cheers for spotting that bug :-) Regarding the frozenness, yeah you're right, but the list never changes after the initial load anyway. I guess if I were being a perfectionist and deciding to ship code in the .NET framework I'd need to handle the list being changed programmatically, but for the purposes of this app it's way overkill – Orion Edwards Aug 06 '10 at 02:17
  • For some reason this does work on controls directly, but not on ControlTemplates. Any ideas on that? – Wouter Aug 27 '12 at 08:38
  • This solution looks good but when I'm not sure where in my XAML file I should use the Conditional Trigger. If the original EventTrigger's source is say, a Rectangle, I should put the ConditionalEventTrigger inside my Rectangle element? Also, I am encountering a compile error, it says: 'The member "Triggers" is not recognizable or not accessible.', any help about this? – Nadavrbn Jan 26 '14 at 12:45
11

This is what worked for me...

I wanted to execute an animation based on the mouse hovering over a UI element and the UI element's associated owner being active (i.e. enabled to make a player move).

To support these requirements, I used relative source binding to overcome the lack of support for event trigger conditions.

Example:

<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>
akjoshi
  • 15,374
  • 13
  • 103
  • 121
Scott Nimrod
  • 11,206
  • 11
  • 54
  • 118
4

Here's my modified version of Ray's answer, which creates and attaches dummy events only when the source triggers are set, rather than doing it each time. I thought this would be better for my scenario as I am raising an event on hundreds of items, not just one or two:

[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> { }

It's used like this:

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

The line

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

is using the reactive framework and some extension methods I wrote, basically we need to subscribe to the .DataContextChanged event of the target object, but we need to do it with a weak reference. If your objects don't ever change their datacontext, then you won't need this code at all

Community
  • 1
  • 1
Orion Edwards
  • 121,657
  • 64
  • 239
  • 328
  • This modification is more efficient but note that it can only be used when: 1. Only one ConditionalEventTrigger is being set, and 2. The Actions collection is never modified after it is initialized. Both of these problems are solvable with additional code. – Ray Burns Aug 04 '10 at 23:42
  • Additional code to allow multiple ConditionalEventTriggers to be used on a single object: Maintain a pool of static RoutedEvent objects. Every time a dummy trigger is constructed, select an event from the pool that is not used in any other EventTrigger on the object. If no such event exists, register a new event and add it to the pool. – Ray Burns Aug 04 '10 at 23:43
  • Additional code to allow Actions collection to be modified: Implement it as ObservableCollection. Store a reference to dummyTrigger in the ConditionalEventTrigger object. When the Action collection notifies of property change, clear and re-fill the action list in dummyTrigger. – Ray Burns Aug 04 '10 at 23:45
3

I know this is an old post, but here is something that worked for me when I ended up here for answers. Basically I wanted a panel that would animate from the right side of the screen on mouse over, and then go back when the mouse left. But, only when the panel wasn't pinned. The IsShoppingCartPinned property is present on my ViewModel. As far as your scenario, you could replace the IsShoppingCartPinned property with your checkbox IsChecked property, and run any kind of animations on the EventTriggers.

Here is the code:

<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>
DannyLee89
  • 31
  • 2
0

in your case you need:

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

EDIT: Based on your comments, you are looking for a multidatatrigger.

   <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>
J Rothe
  • 186
  • 3
  • Thanks for the reply. My actual query is, the animation should happen for all the elements when Mouse is leave from the control. But it should not happen when it is in Checed condition. – Prabu May 10 '10 at 06:32
  • I updated my initial answer. You need to use a multidatatrigger and define you additional conditions. – J Rothe May 12 '10 at 14:18
  • 1
    The trouble with the MultiDataTrigger is that either condition change can cause the animation. For example, in this case if the user's mouse is elsewhere in the UI and they change data causing the checkbox to become checked, the animation will play even though the mouse is nowhere near. (Also note that you omitted the second condition in your MultiDataTrigger, which I'm assuming would be ``) – Ray Burns May 12 '10 at 21:55
  • True I left out the second condition- I expected any other conditions to be filled in as needed as it was only a starting point. That said, I think your condition is inversed- he wants the animation to play for anything where the checkbox is not checked and your condition is executing if true. Perhaps add an additional dependency property to allow for inversing the condition? I suppose he could use a ValueConverter but that seems extraneous for a simple bool operation... – J Rothe May 18 '10 at 15:11
0

Based on Ray and Orion, Here is my version, the goal is that you can bind 2 triggers to a button, and flip the states when click (Or more states if you like, and it should work for all Control). When you bind the ConditionProperty, it's a little tricky that you have to write ConditionValue as False for True, and Ture for False. I guess it's because the event handler of button executed before updating bindings. It used like this:

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

Here is code:

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> {}
}
Hoyt_Ren
  • 11
  • 1
  • I finally realized that if you use a button to flp a state, and then update the UI, you don't need the click event to change the UI, simple let the state change the UI will be OK, this means that use a builtin DataTrigger you would get same result. The only different is event driven or data driven in concept, no real meaning at all. So, let's forget this stupid ConditionalEventTrigger. – Hoyt_Ren Feb 25 '21 at 13:43
  • Oh, no. We still need this. If you animate multiple object, you can do this https://stackoverflow.com/questions/80388/wpf-data-triggers-and-story-boards, but this only works when all objects in the template. If you want to animate another control you still need the EventTrigger due to MS's fault. – Hoyt_Ren Feb 25 '21 at 15:21