wpfxamlrelaycommandrouted-events

Trigger animation when Command is Executed in XAML


I have a View Model that defines a RelayCommand which multiple controls have as their Command binding. I would like to trigger an animation on all the controls that are bound to this command when it executes (or is done executing). The command can be executed by UI controls and in the View Model from a Model event.

Just as an example imagine I want a Button to flash Gold when the MyCommand binding is executed, be it from the button click or somewhere else. A Hyperlink, also bound to MyCommand, would end up making the button flash, although I am not looking for this specific solution (hyperlink triggers button flash directly). Here is the XAML for this example:

<Button Content="My Command"
        Command="{Binding MyCommand}">

    <Button.Background>
        <SolidColorBrush x:Name="buttonBrush"
                         Color="DimGray" />
    </Button.Background>

    <Button.Resources>
        <ColorAnimationUsingKeyFrames x:Key="flash"
                                      Storyboard.TargetProperty="Color">
            <DiscreteColorKeyFrame Value="Gold"
                                   KeyTime="0:0:0" />
            <DiscreteColorKeyFrame Value="Gold"
                                   KeyTime="0:0:0.3" />
            <LinearColorKeyFrame Value="DimGray"
                                 KeyTime="0:0:0.7" />
        </ColorAnimationUsingKeyFrames>
    </Button.Resources>

    <Button.Triggers>
        <EventTrigger RoutedEvent="Binding.Executed">
            <BeginStoryboard>
                <Storyboard Storyboard.TargetName="buttonBrush"
                            Storyboard.TargetProperty="Color">
                    <StaticResource ResourceKey="goldFlash" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Button.Triggers>
</Button>

<TextBlock>
    <Hyperlink Command="{Binding MyCommand}">
        My Command...
    </Hyperlink>
</TextBlock>

I made up RoutedEvent="Binding.Executed" to demonstrate what I am trying to do, I understand that that event doesn't exist.

Update

Following @BionicCode's suggestion I added Executing and Executed events to my RelayCommand class. In my View Model, I receive these events when the command is executed by a button in my UI.

I then added a MyCommandExecuted event to the View Model that gets raised when the command's Executed event occurs.

In my MainWindow, I added an event handler for the MyCommandExecuted event and this event is working properly.

Next I created a RoutedEvent in MainWindow (sorry, this is VB.Net), using code from How to: Create a Custom Routed Event:

' Create a custom routed event by first registering a RoutedEventID
' This event uses the bubbling routing strategy
Public Shared ReadOnly TapEvent As RoutedEvent = EventManager.RegisterRoutedEvent("Tap", RoutingStrategy.Bubble, GetType(RoutedEventHandler), GetType(MainWindow))

' Provide CLR accessors for the event
Public Custom Event Tap As RoutedEventHandler
    AddHandler(ByVal value As RoutedEventHandler)
        Me.AddHandler(TapEvent, value)
    End AddHandler

    RemoveHandler(ByVal value As RoutedEventHandler)
        Me.RemoveHandler(TapEvent, value)
    End RemoveHandler

    RaiseEvent(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Me.RaiseEvent(e)
    End RaiseEvent
End Event

and I raise this event in the MyCommandExecuted event handler:

' This method raises the Tap event
Private Sub RaiseTapEvent()
    Dim newEventArgs As New RoutedEventArgs(MainWindow.TapEvent)
    MyBase.RaiseEvent(newEventArgs)
End Sub

' For demonstration purposes we raise the event when the MyButtonSimple is clicked
Private Sub MyCommandExecuted() Handles _myViewModel.MyCommandExecuted
    Me.RaiseTapEvent()
End Sub

This code gets executed so everything up to here is working. Finally, in the XAML, I created an EventTrigger for local:MainWindow.Tap:

<Border Width="100" Height="100" x:Name="MyBorder" Background="AliceBlue">
    <Border.Triggers>
        <EventTrigger RoutedEvent="local:MainWindow.Tap">
            <BeginStoryboard>
                <Storyboard>
                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="MyBorder"
                                                  Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                                  FillBehavior="Stop"
                                                  Duration="0:0:2">
                        <DiscreteColorKeyFrame Value="Gold"
                                               KeyTime="0:0:0" />
                        <DiscreteColorKeyFrame Value="DarkOrange"
                                               KeyTime="0:0:1" />
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Border.Triggers>
</Border>

This doesn't work... events don't "bubble down" (makes sense) but if you put the EventTrigger at the Window level like in @BionicCode's example, it works, so problem solved.


Solution

  • You should implement a MyCommandExecuted event in the view model. The UI component that is executing the animation, should listen to this event e.g. by subscribing to the view model which is the current DataContext.

    The event handler of MyCommandExecuted then starts the animation preferably by raising a routed event e.g., CommandExecuted which will be handled by the corresponding EventTrigger.
    Alternatively you could start the animation directly in code-behind when handling the MyCommandExecuted event. But using EventTrigger in XAML is much more convenient.

    Example

    ViewModel.cs

    public class ViewModel : INotifyPropertyChanged
    {
      public ICommand MyCommand => new RelayCommand(ExecuteMyCommand, (param) => true);
      public event EventHandler MyCommandExecuted;
    
      private void ExecuteMyCommand(object obj)
      {
        // TODO::Implement command operation ...
    
        OnMyCommandExecuted();
      }
    
      protected virtual void OnMyCommandExecuted()
      {
        this.MyCommandExecuted?.Invoke(this, EventArgs.Empty);
      }
    }
    

    MainWindow.xaml.cs

    partial class MainWindow : Window
    {
      #region Routed Events
    
      public static readonly RoutedEvent AnimationRequestedRoutedEvent = EventManager.RegisterRoutedEvent(
        "AnimationRequested",
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(MainWindow));
    
      public event RoutedEventHandler AnimationRequested
      {
        add => AddHandler(MainWindow.AnimationRequestedRoutedEvent, value);
        remove => RemoveHandler(MainWindow.AnimationRequestedRoutedEvent, value);
      }
    
      #endregion Routed Events
    
      public MainWindow()
      {
        InitializeComponent();
    
        var viewModel = new ViewModel();
        viewModel.MyCommandExecuted += TriggerAnimation_OnCommandExecuted;
    
        this.DataContext = viewModel;
      }
    
      private void TriggerAnimation_OnCommandExecuted(object sender, EventArgs e)
      {
        RaiseEvent(new RoutedEventArgs(MainWindow.AnimationRequestedRoutedEvent, this));
      }
    }
    

    MainWindow.xaml

    <Window>
      <Button x:Name="AnimatedButton" 
              Content="Execute My Command"
              Command="{Binding MyCommand}" 
              Background="DimGray" />
    
      <Window.Triggers>
        <EventTrigger RoutedEvent="MainWindow.AnimationRequested">
          <BeginStoryboard>
            <Storyboard>
              <ColorAnimationUsingKeyFrames Storyboard.TargetName="AnimatedButton"
                                            Storyboard.TargetProperty="(Button.Background).(SolidColorBrush.Color)" 
                                            FillBehavior="Stop"
                                            Duration="0:0:0.7">
                <DiscreteColorKeyFrame Value="Gold"
                                       KeyTime="0:0:0" />
                <DiscreteColorKeyFrame Value="DarkOrange"
                                       KeyTime="0:0:0.3" />
              </ColorAnimationUsingKeyFrames>
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </Window.Triggers>
    </Window>