Search code examples
wpfcanvasstoryboardeventtriggerelementhost

WPF - Configure custom event to trigger begin storyboard on user control


I hope someone is able to assist here, either to advise how to fix what I have or to suggest a better way of achieving what I'm after.

I have a simple custom control on which I want to be able to start/stop/pause/resume an opacity animation. I've looked at various solutions and figured that using EventTrigger with a custom RoutedEvent was the way forward. I can get the following to work with a standard event (eg MouseDown) but am unable to get my custom event to start the storyboard.

I tried raising the event in the constructor prior to doing it in a loaded event, but neither makes any difference.

I don't think it has any relevant impact but this is also being hosted in an ElementHost for winforms.

xaml:

<UserControl x:Class="Fraxinus.BedManagement.WpfControls.FraxIconX"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Fraxinus.BedManagement.WpfControls"
             mc:Ignorable="d" 
             d:DesignHeight="16" d:DesignWidth="16"
             Loaded="FraxIconX_Loaded">
    <!--Height="16" Width="16" Background="{Binding Path=Background, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Control}, AncestorLevel=1}}" -->
    <UserControl.Resources>
        <ResourceDictionary>
            <Border x:Key="RoundedBorderMask" Name="iconBorder" CornerRadius="4" Background="White" BorderThickness="2" Height="16" Width="16" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Dictionary.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <Border BorderThickness="2" CornerRadius="4" Height="16" Width="16" HorizontalAlignment="Center" VerticalAlignment="Center">
        <local:FraxCanvas x:Name="fraxCanvas" Height="16" Width="16" HorizontalAlignment="Center" VerticalAlignment="Center">
            <local:FraxCanvas.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1" GradientStops="{Binding GradientStops}" />
            </local:FraxCanvas.Background>
            <local:FraxCanvas.Triggers>
                <EventTrigger RoutedEvent="local:FraxCanvas.OnStartStoryboard" SourceName="fraxCanvas">
                    <BeginStoryboard x:Name="storyboard">
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetProperty="Opacity" From="1" To="0.25" AutoReverse="True" Duration="0:0:1" RepeatBehavior="Forever"></DoubleAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </local:FraxCanvas.Triggers>
            <local:FraxCanvas.OpacityMask>
                <!--https://wpf.2000things.com/2012/05/11/556-clipping-to-a-border-using-an-opacity-mask/-->
                <VisualBrush Visual="{StaticResource RoundedBorderMask}"></VisualBrush>
            </local:FraxCanvas.OpacityMask>
            <Image HorizontalAlignment="Center" VerticalAlignment="Center" Height="16" Width="16" Source="{DynamicResource BitmapClockPng}" />
        </local:FraxCanvas>
    </Border>
</UserControl>

Code behind

    public partial class FraxIconX : UserControl
    {

        public GradientStopCollection GradientStops { get; set; }

        public FraxIconX() { }

        public FraxIconX(Color backColour, int width = 16, int height = 16)
        {
            Width = width;
            Height = height;
            GradientStops = new GradientStopCollection { new GradientStop(Colors.AliceBlue, 0.0), new GradientStop(Colors.Navy, 1.0) };
            Background = new SolidColorBrush(backColour);
            DataContext = this;
            InitializeComponent();
        }
        public void FraxIconX_Loaded(object sender, RoutedEventArgs args)
        {
            RaiseEvent(new RoutedEventArgs(FraxCanvas.StartStoryboardEvent));
        }
    }

FraxCanvas:

    public class FraxCanvas : Canvas
    {
        public static readonly RoutedEvent StartStoryboardEvent = EventManager.RegisterRoutedEvent("OnStartStoryboard", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(FraxCanvas));

        public event RoutedEventHandler OnStartStoryboard
        {
            add
            {
                this.AddHandler(StartStoryboardEvent, value);
            }

            remove
            {
                this.RemoveHandler(StartStoryboardEvent, value);
            }
        }
    }

Solution

  • Two issues.

    1. You are listening exclusively to EventTrigger.SourceName="fraxCanvas", but the event is raised somewhere else (parent UserControl).
      EventTrigger.SourceName is a filter property, which allows to exclusively handle a Routed Event of a specific element. If you don't set EventTrigger.SourceName, the trigger will handle the specified event, no matter the source.
    2. The Routed Event will never reach the FraxCanvas element, because a parent raised it.
      RoutingStrategy.Bubble: event travels from event source, the FraxIconX element, to the visual tree root e.g., Window.
      RoutingStrategy.Tunnel: event travels from the tree root e.g., Window and stops at the event source, the FraxIconX element.

    The solution is to remove the EventTrigger.SourceName attribute and move the trigger to the event source (the UserControl) or to any of the event source's parent element:

    <UserControl>    
      <UserControl.Triggers>
        <EventTrigger RoutedEvent="local:FraxCanvas.OnStartStoryboard">
          <BeginStoryboard x:Name="storyboard">
            <Storyboard>
              <DoubleAnimation Storyboard.TargetName="fraxCanvas" 
                               Storyboard.TargetProperty="Opacity" 
                               From="1" To="0.25" />
            </Storyboard>
          </BeginStoryboard>
        </EventTrigger>
      </UserControl.Triggers>
    
      <Border>
        <FraxCanvas x:Name="fraxCanvas" />
      </Border>
    </UserControl>