I'm trying to achieve a dynamic, keyframes-based color animation of my own custom ToggleButton's Background property (WPF custom control inheriting a ToggleButton). I need the animation to have its duration and colors (from/to) customizable at runtime, so I have exposed three dependency properties to alter these parameters using binding in XAML. When value of either of them changes, I update the animation to be fired when Blinking (bool DP) is set to true.
I tried following approach:
public class CommandAndActivationButton : ToggleButton
{
public static readonly DependencyProperty BlinkingProperty = DependencyProperty.Register("Blinking", typeof(bool), typeof(CommandAndActivationButton), new PropertyMetadata(default(bool), OnBlinkingChanged));
internal static readonly DependencyProperty BlinkingDurationProperty = DependencyProperty.Register("BlinkingDuration", typeof(Duration), typeof(CommandAndActivationButton), new PropertyMetadata(default(Duration), OnAnimationParametersChanged));
internal static readonly DependencyProperty BlinkAnimationColorBrushProperty = DependencyProperty.Register("BlinkAnimationColorBrush", typeof(SolidColorBrush), typeof(CommandAndActivationButton), new PropertyMetadata(default(SolidColorBrush), OnAnimationParametersChanged));
internal static readonly DependencyProperty InitialAnimationColorBrushProperty = DependencyProperty.Register("InitialAnimationColorBrush", typeof(SolidColorBrush), typeof(CommandAndActivationButton), new PropertyMetadata(default(SolidColorBrush), OnAnimationParametersChanged));
private ColorAnimationUsingKeyFrames m_BackgroundAnimation;
static CommandAndActivationButton()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CommandAndActivationButton),
new FrameworkPropertyMetadata(typeof(CommandAndActivationButton)));
}
/// <summary>
/// Whether the button should currently be blinking.
/// </summary>
public bool Blinking
{
get => (bool)GetValue(BlinkingProperty);
set => SetValue(BlinkingProperty, value);
}
/// <summary>
/// Duration (period) of the blinking animation.
/// </summary>
public Duration BlinkingDuration
{
get => (Duration)GetValue(BlinkingDurationProperty);
set => SetValue(BlinkingProperty, value);
}
/// <summary>
/// Color for use in blinking animation (i.e. the "to" color of the animation).
/// </summary>
public SolidColorBrush InitialAnimationColorBrush
{
get => (SolidColorBrush)GetValue(InitialAnimationColorBrushProperty);
set => SetValue(InitialAnimationColorBrushProperty, value);
}
/// <summary>
/// Color for use in blinking animation (i.e. the "to" color of the animation).
/// </summary>
public SolidColorBrush BlinkAnimationColorBrush
{
get => (SolidColorBrush)GetValue(BlinkAnimationColorBrushProperty);
set => SetValue(BlinkAnimationColorBrushProperty, value);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (!Blinking) { return; }
StartAnimation();
}
private static void OnBlinkingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is CommandAndActivationButton c)) { return; }
if ((bool)e.NewValue)
{
c.StartAnimation();
}
else
{
c.StopAnimation();
}
}
private static void OnAnimationParametersChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is CommandAndActivationButton c)) { return; }
// Make sure we have enough data to create animation
if (c.BlinkAnimationColorBrush == null ||
c.InitialAnimationColorBrush == null ||
c.BlinkingDuration == default) { return; }
c.m_BackgroundAnimation = new ColorAnimationUsingKeyFrames
{
Duration = c.BlinkingDuration,
RepeatBehavior = RepeatBehavior.Forever,
FillBehavior = FillBehavior.Stop,
AutoReverse = true,
KeyFrames = new ColorKeyFrameCollection
{
new DiscreteColorKeyFrame(c.InitialAnimationColorBrush.Color, KeyTime.FromPercent(0.0)),
new DiscreteColorKeyFrame(c.BlinkAnimationColorBrush.Color, KeyTime.FromPercent(1.0))
}
};
}
private void StartAnimation()
{
Background.BeginAnimation(SolidColorBrush.ColorProperty, m_BackgroundAnimation);
}
private void StopAnimation()
{
Background.BeginAnimation(SolidColorBrush.ColorProperty, null);
}
}
Other than this animation, the button also has a number of data triggers that change its Background when toggled (IsChecked = true), on mouse hover etc., but as far as I know those behaviors are overridden while an animation is running, so I should get the behavior I want... but I don't (although StartAnimation() is called correctly at runtime).
I have already tried to achieve the same using a single animation in pure-XAML but seems to be impossible as all the animations properties in XAML need to be static (otherwise I get a "Cannot freeze this Storyboard timeline tree for use across threads" error connected to animation being a Freezable).
Do you have an idea what am I doing wrong here?
Edit: It does not even run with hard-coded duration and colors, like below (minimum example: setting Blinking to true correctly causes a call to StartAnimation() on the button, but nothing happens with the button's background):
public class CommandAndActivationButton : ToggleButton
{
public static readonly DependencyProperty BlinkingProperty = DependencyProperty.Register("Blinking", typeof(bool), typeof(CommandAndActivationButton), new PropertyMetadata(default(bool), OnBlinkingChanged));
static CommandAndActivationButton()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(CommandAndActivationButton),
new FrameworkPropertyMetadata(typeof(CommandAndActivationButton)));
}
/// <summary>
/// Whether the button should currently be blinking.
/// </summary>
public bool Blinking
{
get => (bool)GetValue(BlinkingProperty);
set => SetValue(BlinkingProperty, value);
}
private static void OnBlinkingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is CommandAndActivationButton c)) { return; }
if ((bool)e.NewValue)
{
c.StartAnimation();
}
}
private void StartAnimation()
{
var animation = new ColorAnimationUsingKeyFrames
{
Duration = new Duration(TimeSpan.FromSeconds(1)),
RepeatBehavior = RepeatBehavior.Forever,
FillBehavior = FillBehavior.Stop,
AutoReverse = true,
KeyFrames = new ColorKeyFrameCollection
{
new DiscreteColorKeyFrame(Colors.Blue, KeyTime.FromPercent(0.0)),
new DiscreteColorKeyFrame(Colors.Yellow, KeyTime.FromPercent(0.5)),
new DiscreteColorKeyFrame(Colors.Blue, KeyTime.FromPercent(1.0))
}
};
Background.BeginAnimation(SolidColorBrush.ColorProperty, animation);
}
}
The corresponding ControlTemplate of that button looks like this (simplified):
<!-- Main button template -->
<Style TargetType="{x:Type local:CommandAndActivationButton}"
BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Background">
<Setter.Value>
<SolidColorBrush Color="Gray"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:CommandAndActivationButton">
<Border Background="{TemplateBinding Background}">
<!-- Stuff (with not set/transparent background) -->
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="Opacity" Value="0.7"/>
</Trigger>
<ControlTemplate.Triggers>
</ControlTemplate>
<!-- Not important -->