0

I'm trying to create a simple change in the background color of a border based on a timer.

I have this XAML:(That I pretty much copied from this answer)

    <Grid>
        <Border CornerRadius="50"
                x:Name="ledColor"
                BorderBrush="Black"
                Background="Gray"
                BorderThickness="5">
            <Border.Triggers>
                <EventTrigger RoutedEvent="Border.MouseLeave"> //Event that triggers the animation
                    <BeginStoryboard>
                        <Storyboard>
                            <ColorAnimation To="LightGreen"
                                            Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                            FillBehavior="Stop"
                                            Duration="0:0:0.1"
                                            AutoReverse="True"></ColorAnimation>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Border.Triggers>
            <Viewbox Stretch="Uniform"
                     VerticalAlignment="Center"
                     HorizontalAlignment="Center">
                <Label Content="Tx" />
            </Viewbox>
        </Border>
    </Grid>

Which works just fine for the built-in MouseLeave event. When I hover over the control and then move the mouse outside, the background color changes to green and then returns to its original state.

enter image description here

However, what I want to do is to trigger the animation based on a custom event.

In the code behind I made a timer with a 1000ms interval. I want to trigger the animation on the timer's 'Elapsed' event.

        public partial class LedIndicator : UserControl
    {
        private System.Timers.Timer _timer;
              

        public Timer Timer { get => _timer; set => _timer = value; }

        public LedIndicator()
        {
            InitializeComponent();
           
            Timer = new System.Timers.Timer
            {
                AutoReset = true,
                Interval = 1000
            };
            Timer.Elapsed += OnTimerElapsed;
            Timer.Start();
        }

        public void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            Debug.WriteLine($"Timer elapsed");
        }
    }

I wrapped the timer in a Property in the hope of somehow exposing its Elapse event to the XAML. But I have no idea how to do it.

Curtwagner1984
  • 1,908
  • 4
  • 30
  • 48
  • From my guess you should need to create a Storyboard via code, then call `ledColor.BeginStoryboard(myAnimation)` from within your `OnTimerElapsed`. you may need to do the `BeginStoryboard` from within the Application Dispatcher as I believe the Timer will execute on a different thread (depending with Timer you use) from the main UI thread. – Ginger Ninja Jul 27 '22 at 17:25
  • @GingerNinja One Idea I had was to create some property like `bool IsOn {get; set;}` and implement `INotifyPropertyChanged` and then use this property as a DataTrigger for the animation, and set it inside the TimerElapsed event. But it seems like an ugly workaround. – Curtwagner1984 Jul 27 '22 at 17:29
  • You dont need to do that. have you tried using the `System.Threading.Timer` instead of `System.Timer.Timer`? – Ginger Ninja Jul 27 '22 at 18:52
  • Cyclically triggering the start of an animation from a timer does not make much sense, when you consider setting its RepeatBehavior to Forever, and possible also set AutoReverse to true. A "blinking LED" is the perfect use case for an infinitely running animation. – Clemens Jul 27 '22 at 18:53
  • However, when you use a timer, it should of course be a DispatcherTimer. – Clemens Jul 27 '22 at 18:54
  • @Clemens I use a time to conveniently trigger a custom event. In a real use case scenario another event would be fired. I just want to get to a point where I can trigger the animation with an event. The timer part is not the important part here. The question is how to trigger the animation with an event? – Curtwagner1984 Jul 27 '22 at 19:01
  • @GingerNinja "have you tried using the System.Threading.Timer instead of System.Timer.Timer" I did not. But I don't see how it will change things given that I don't know how to expose the elapsed event to the XAML. – Curtwagner1984 Jul 27 '22 at 19:03

2 Answers2

1

Here is a rough example

Create a Brush and Storyboard in code somewhere:

private SolidColorBrush myChangingBackground = new SolidColorBrush();

//at some point set the Background of the Button to this Brush in code behind. Loaded Event or maybe the Constructor
ledColor.Background = myChangingBackground;

private myAnimation = new ColorAnimation(Colors.LightGreen, new Duration(new TimeSpan(0, 0, 1)), FillBehavior.Stop);
myAnimation.AutoReverse = true;    

Setup the timer:

private System.Threading.Timer animationTimer = new Timer(_ => 
{
    //need dispatcher to execute on UI thread
    Dispatcher.Invoke(() => 
    {
       myChangingBackground.BeginAnimation(SolidColorBrush.ColorProperty, myAnimation);
    });
}, 0, 1000); //start immediately, and elapse every 1 second
Ginger Ninja
  • 759
  • 8
  • 21
  • As Clemens said, you can also use a DistpatcherTimer. Figured I would use a raw timer incase the event you are wanting to trigger from is on a separate thread. – Ginger Ninja Jul 27 '22 at 19:11
  • Hey, I understand the idea behind this solution, but I'm getting an exception saying that `ColorAnimation` can't be used to animate the property Background, because it's of type 'brush' System.ArgumentException HResult=0x80070057 Message=AnimationTimeline of type 'System.Windows.Media.Animation.ColorAnimation' cannot be used to animate the 'Background' property of type 'System.Windows.Media.Brush'. (Parameter 'animation') – Curtwagner1984 Jul 28 '22 at 07:13
  • Yeah, didnt really check if a ColorAnimation can change the Brush. So you would need to create a Brush, Bind that brush with the ColorAnimation, then Bind that Brush to the Background. I'll edit the post to show an example – Ginger Ninja Jul 28 '22 at 15:35
  • 1
    Thank you! I've made an answer too by googling, you can see it below. But I'm sure your example will be better. – Curtwagner1984 Jul 28 '22 at 17:19
1

This is a small modification of @Ginger Ninja's answer to a working example.

When I tried his answer out, I got an exception saying that you can't animate the background property of type Brush with ColorAnimation.

After some Googleing, I found this post about animating a background.

Saying that you actually should animate the Brush like so:

SolidColorBrush brush = new SolidColorBrush(Colors.Red);
buttonName.Background = brush;
 
ColorAnimation animation = new ColorAnimation(Colors.Red, Colors.Blue, 
               new Duration(TimeSpan.FromSeconds(5)));
 
animation.AutoReverse = true;
animation.RepeatBehavior = RepeatBehavior.Forever;
brush.BeginAnimation(SolidColorBrush.ColorProperty, animation);

But I did not want to set the initial Brush color in code so I tried to do something like this:

SolidColorBrush backgroundBrush = (SolidColorBrush)ledColor.Background;
backgroundBrush.BeginAnimation(SolidColorBrush.ColorProperty, _colorAnimation);

But this resulted in throwing another exception saying that you can't animated 'Frozen or Sealed' objects.

This answer said you should give a new mutable instance of a SolidColorBrush to the object you wish to animate. And this is what I did:

public partial class LedIndicator : UserControl
    {
        private System.Timers.Timer _timer;

        private readonly ColorAnimation _colorAnimation;

        public Timer Timer { get => _timer; set => _timer = value; }

        public LedIndicator()
        {
            InitializeComponent();

            _colorAnimation = new ColorAnimation(Colors.LightGreen, new Duration(new TimeSpan(0, 0, 0, 0, 50)), FillBehavior.Stop)
            {
                AutoReverse = true
            };

            Timer = new System.Timers.Timer
            {
                AutoReset = true,
                Interval = 500
            };
            Timer.Elapsed += OnTimerElapsed;
            Timer.Start();

        }

        public void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
        {
            Debug.WriteLine($"Timer elapsed");
            Dispatcher.Invoke(() =>
            {
                
                SolidColorBrush brush = new(((SolidColorBrush)ledColor.Background).Color);
                ledColor.Background = brush; //replace the original brush with a new mutable brush of the same color.
                brush.BeginAnimation(SolidColorBrush.ColorProperty, _colorAnimation); // start animation
            });
            

        }
    }


This is the result:

enter image description here

Curtwagner1984
  • 1,908
  • 4
  • 30
  • 48