As this seems to be a bug in the implementation of the ChangePropertyAction class, I figured the best way to get to the bottom of this is to throw the assembly into your favorite reflector style app, and look at the guts of the implementation.
Here's an excerpt (there's a lot left out, but the relevant bit is in there though):
public class ChangePropertyAction : TargetedTriggerAction<object>
{
/* some dependency properties here, like DurationProperty, ValueProperty, etc... */
protected override void Invoke(object parameter)
{
/* a lot of validation here, but skimming over that mostly. Valid input results in a call to AnimatePropertyChange() */
}
private void AnimatePropertyChange(PropertyInfo propertyInfo, object fromValue, object newValue)
{
Storyboard storyboard = new Storyboard();
Timeline timeline = !typeof (double).IsAssignableFrom(propertyInfo.PropertyType)
? (!typeof (Color).IsAssignableFrom(propertyInfo.PropertyType)
? (!typeof (Point).IsAssignableFrom(propertyInfo.PropertyType)
? this.CreateKeyFrameAnimation(fromValue, newValue)
: this.CreatePointAnimation((Point) fromValue, (Point) newValue))
: this.CreateColorAnimation((Color) fromValue, (Color) newValue))
: this.CreateDoubleAnimation((double) fromValue, (double) newValue);
timeline.Duration = this.Duration;
storyboard.Children.Add(timeline);
Storyboard.SetTarget((Timeline) storyboard, (DependencyObject) this.Target);
Storyboard.SetTargetProperty((Timeline) storyboard, new PropertyPath(propertyInfo.Name, new object[0]));
storyboard.Completed += (EventHandler) ((o, e) => propertyInfo.SetValue(this.Target, newValue, new object[0]));
storyboard.FillBehavior = FillBehavior.Stop;
storyboard.Begin();
}
private static object GetCurrentPropertyValue(object target, PropertyInfo propertyInfo)
{
FrameworkElement frameworkElement = target as FrameworkElement;
target.GetType();
object obj = propertyInfo.GetValue(target, (object[]) null);
if (frameworkElement != null && (propertyInfo.Name == "Width" || propertyInfo.Name == "Height") && double.IsNaN((double) obj))
obj = !(propertyInfo.Name == "Width") ? (object) frameworkElement.ActualHeight : (object) frameworkElement.ActualWidth;
return obj;
}
private Timeline CreateDoubleAnimation(double fromValue, double newValue)
{
return (Timeline) new DoubleAnimation()
{
From = new double?(fromValue),
To = new double?(newValue),
EasingFunction = this.Ease
};
}
}
If you want to look at the full code, run it through DotPeek or ILSpy yourself, both are free :-)
So in the end, all it's doing is validating inputs, looking at the type of the value and create a storyboard with a transition animation appropriate to the property type.
The 'flicker' effect is actually the value briefly returning to its original value (that which is actually bound) when the animation is done, after which the binding is updated to reflect the new value.
The reason for this behavior is down to one single property setting on the storyboard:
storyboard.FillBehavior = FillBehavior.Stop;
This FillBehavior determines what happens when the Timeline (the storyboard in this case) reaches its end. MSDN has this to say:
HoldEnd: After it reaches the end of its active period, the timeline
holds its progress until the end of its parent's active and hold
periods.
Stop: The timeline stops if it is outside its active period while its
parent is inside its active period.
If we simply change this property to be set to FillBehavior.HoldEnd, the flicker is gone. The downside is that you'll have to re-implement this TriggerAction, but you can probably leave out a lot if you just want it to work for a double animation.
Hope this helps anyone!