You should extend ColorAnimation
to keep the default animation behavior or implementation details in general.
For example, create a DynamicColorAnimation
class that extends ColorAnimation
and define binding properties on it. The point is that you can't set Binding
expressions inside a Storyboard
as this would prevent the Freezable
(in this case the ColorAnimation
) from being frozen. To solve this, you can just collect the actual Binding
values and then resolve them explicitly during runtime.
The behavior is similar to the e.g. DataGridBoundColumn.Binding
property (for example of the extended DataGridTextColumn
).
In our case we have two such binding properties named FromBinding
and ToBinding
. And because we won't define those properties as dependency properties, the Freezable
will ignore the Binding
values (allowing the DynamicColorAnimation
to be frozen as required): the Binding
in this case is not evaluated by the dependency property system and is instead treated like an ordinary property value.
If DynamicColorAnimation.FromBinding
or DynamicColorAnimation.ToBinding
is not set, the DynamicColorAnimation
behaves like a normal ColorAnimation
and falls back to the ColorAnimation.From
or ColorAnimation.To
property values.
ColorAnimation.From
and ColorAnimation.To
have precedence over DynamicColorAnimation.FromBinding
and DynamicColorAnimation.ToBinding
.
Both can be mixed, for example FromBindng
and To
.
public class DynamicColorAnimation : ColorAnimation
{
// Helper class to resolve bindings
internal class BindingResolver<TValue> : DependencyObject
{
// Define property of type 'object' to make it nullable by default.
// Introduces boxing in case 'TValue' is a 'ValueType'.
public object ResolvedBindingValue
{
get => (object)GetValue(ResolvedBindingValueProperty);
set => SetValue(ResolvedBindingValueProperty, value);
}
public static readonly DependencyProperty ResolvedBindingValueProperty = DependencyProperty.Register(
"ResolvedBindingValue",
typeof(object),
typeof(BindingResolver<TValue>),
new PropertyMetadata(default));
// Returns 'false' when the binding couldn't be resolved
public bool TryResolveBinding(BindingBase bindingBase, out TValue? resolvedBindingValue)
{
_ = BindingOperations.SetBinding(this, ResolvedBindingValueProperty, bindingBase);
resolvedBindingValue = (TValue)this.ResolvedBindingValue;
return this.ResolvedBindingValue is not null;
}
}
public BindingBase? FromBinding { get; set; }
public BindingBase? ToBinding { get; set; }
private BindingResolver<Color> BindingValueProvider { get; }
public DynamicColorAnimation()
{
this.BindingValueProvider = new BindingResolver<Color>();
}
// Because we extend ColorAnimation which is a Freezable,
// we must override Freezable.CreateInstanceCore too
protected override Freezable CreateInstanceCore()
=> new DynamicColorAnimation();
protected override Color GetCurrentValueCore(Color defaultOriginValue, Color defaultDestinationValue, AnimationClock animationClock)
{
Color fromColor = this.From ?? defaultOriginValue;
Color toColor = this.To ?? defaultDestinationValue;
if (!this.From.HasValue
&& this.FromBinding is not null)
{
// Ignore the default value to give the defaultOriginValue
// parameter precedence in case the binding didn't resolve
if (this.BindingValueProvider.TryResolveBinding(this.FromBinding, out Color bindingValue))
{
fromColor = bindingValue;
}
}
if (!this.To.HasValue
&& this.ToBinding is not null)
{
// Ignore the default value to give the defaultOriginValue
// parameter precedence in case the binding didn't resolve
if (this.BindingValueProvider.TryResolveBinding(this.ToBinding, out Color bindingValue))
{
toColor = bindingValue;
}
}
return base.GetCurrentValueCore(fromColor, toColor, animationClock);
}
}
Usage Example
<Style x:Key="ButtonStyle"
TargetType="Button">
<Style.Resources>
<!-- Some dynamic resources -->
<SolidColorBrush x:Key="FromColor"
Color="DarkRed" />
<SolidColorBrush x:Key="ToColor"
Color="Orange" />
</Style.Resources>
<Style.Triggers>
<EventTrigger RoutedEvent="MouseMove">
<BeginStoryboard>
<Storyboard>
<!-- This example binds to the dynamic SolidColorBrush resources.
Of course, the bindings can use anything as source as usual -->
<DynamicColorAnimation Storyboard.TargetProperty="Background.Color"
Duration="0:0:5"
FillBehavior="Stop"
FromBinding="{Binding Source={StaticResource FromColor}, Path=Color}"
ToBinding="{Binding Source={StaticResource ToColor}, Path=Color}">
</local:DynamicColorAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>