Introduction
This article was conceived as an answer to my old question to Microsoft: “Why are there so many duplications in .NET code and why not make it Generic based?” If you’ll take, for example, class DoubleAnimation
and replace all instances of the word “Double
” to the word “Color
”, you’ll receive ColorAnimation
class exactly. You’ll receive the same for most animations. Some very specific classes can be slightly different but most of the code is duplicated.
The other reason for this article (and a motivator for me) is the comments for another article on this site “WPF Tutorial - Part 2: Writing a custom animation class”. The goal of that article is to explain “how to” rather than to give a fully functional solution. So I decided to do that here.
Why Do We Need It?
Most standard types are covered by existing Animation
classes, but sometimes we encounter a non-standard property, which in theory can be animated in the same or similar way, but it requires a custom class for animation. To maximally simplify this dirty work, I built some generic classes that allow making animations similar to DoubleAnimation
for any relevant type. In my sample, I added animations for GridLength
, CornerRadius
, LinearGradientBrush
and RadialGradientBrush
. The GridLength
animation animates Grid
row and column size. For example, you can use star based units to animate from {*} to {3.5*}. The CornerRadius
animation can be used with the property of the same name of the class Border
. The idea
(and samples) of the LinearGradientBrush
animation was found in another article. It was implemented on my code base and supplemented by it sibling RadialGradientBrush
animation.
These animations can be used to animate whole brush instead of colors or individual
properties.
Supported Animations
There are three types of animations that exist in .NET:
- Transitions (
*Animation
classes)
- Key-frame animations (
*AnimationUsingKeyFrames
classes)
- Path animations (
*AnimationUsingPath
classes)
My classes covered the first and second types. The third type is very specific for different value types.
Implementation Troubleshoots
Nullable Problem
Implementation of the Animation<…>
class required usage of Nullable
for ValueType
. On the other hand, I wanted to do the same implementation for reference and value types. I didn’t find a built in way to implement it for the generic classes. So I added the generic parameter TNullableValue
and introduced the common class NullableHelper
, which allows manipulation of value and corresponding nullable value the same way for reference and value types. To hide nullable problems and simplify inheritance and usage, I wrapped the Animation
class with two inheritors for reference and value types: ValueTypeAnimation
and RefTypeAnimation
. They receive the value type and generate appropriated nullable type inside.
Generics Problem
Implementation of the AnimationUsingKeyFrame<…>
class made plain the intolerance of WPF and especially Expression Blend 4 to generic types. The problem revealed itself when I used generic key frame collection as the content property of the animation class. Visual Studio just underlined key frames as warnings and Expression Blend refused compilation and presentation of the view and even crashed on compilation. The solution was simple, but a bit awkward. I introduced an additional parameter for AnimationUsingKeyFrame<…>
class – TKeyFrameCollection
. Now you need to inherit from any collection type which is derived from Freesable
and implements IList
, IList< KeyFrame<TValue>>
. It can be FreezableCollection
as in my samples or you can use your own (this is a little advantage). This non-generic type needs to be specified as the last parameter of the animation class.
How to Use?
To create your own animations, you need to do the following steps:
IAnimationHelper
This interface is the most valuable part to implement, because it defines the behavior of the animation and defines correct calculations for your type. It assumes all values are non-null
. NullableHelper
solves Nullable problems out of this interface.
It declares the following functions:
IsValidValue
– verifies value for validity (e.g. GridLengs
is invalid if it has Auto
type; Double
cannot be NaN
or infinity, etc.)
GetZeroValue
– should return valid value, when adding to or subtracting from another value leaves that other value unchanged
AddValues
– should return sum of two values
SubtractValue
– should subtract values
ScaleValue
– should multiply value by factor
InterpolateValue
– should interpolate value according to scale and progress
GetSegmentLength
– determines the length of segment between key frames if key frame time type defined as KeyTimeType.Paced
. If segment length cannot be calculated, returns 1.0 for different values and 0.0 for same values.
IsAccumulable
– determines validity of usage of IsAdditive
and IsCumulative
properties of the animation (e.g. String
is not accumulable, but Double
is.)
Implementation Example
I'll show here implementation for GridLength.
First of all implementing IAnimationHelper
public sealed class GridLengthAnimationHelper : IAnimationHelper<GridLength>
{
#region IAnimationHelper
public bool IsValidValue(GridLength value) { return !value.IsAuto; }
public GridLength GetZeroValue() { return new GridLength(0); }
public GridLength AddValues(GridLength value1, GridLength value2)
{
var targetType = VerifyCompatibility(value1, value2);
return new GridLength(value1.Value + value2.Value, targetType);
}
public GridLength SubtractValue(GridLength value1, GridLength value2)
{
var targetType = VerifyCompatibility(value1, value2);
return new GridLength(value1.Value - value2.Value, targetType);
}
public GridLength ScaleValue(GridLength value, double factor)
{
if (value.IsAuto)
throw new InvalidOperationException("Cannot animate GridLengs with Auto type");
return new GridLength(value.Value * factor, value.GridUnitType);
}
public GridLength InterpolateValue(GridLength from, GridLength to, double progress)
{
var targetType = VerifyCompatibility(from, to);
return new GridLength(from.Value + ((to.Value - from.Value) * progress), targetType);
}
public double GetSegmentLength(GridLength from, GridLength to)
{
VerifyCompatibility(from, to);
return Math.Abs(to.Value - from.Value);
}
bool IAnimationHelper<GridLength>.IsAccumulable { get { return true; } }
#endregion IAnimationHelper
private static GridUnitType VerifyCompatibility(GridLength value1, GridLength value2)
{
if (value2.Value.CompareTo(0.0) == 0)
return value1.GridUnitType;
if (value1.Value.CompareTo(0.0) == 0)
return value2.GridUnitType;
if (value1.GridUnitType != value2.GridUnitType)
throw new InvalidOperationException("Using of different GridLengs types");
return value1.GridUnitType;
}
}
Now can be created animations:
public class GridLengthAnimation : ValueTypeAnimation<GridLength, GridLengthAnimationHelper>
{
#region Freezable
public new GridLengthAnimation Clone() { return (GridLengthAnimation)base.Clone(); }
protected override Freezable CreateInstanceCore() { return new GridLengthAnimation(); }
#endregion
}
public class GridLengthKeyFrameCollection : FreezableCollection<KeyFrame<GridLength>>
{
#region Freezable
protected override Freezable CreateInstanceCore() { return new GridLengthKeyFrameCollection(); }
#endregion
}
public class GridLengthAnimationUsingKeyFrames : AnimationUsingKeyFrames<GridLength, GridLengthAnimationHelper, GridLengthKeyFrameCollection>
{
#region Freezable
public new GridLengthAnimationUsingKeyFrames Clone() { return (GridLengthAnimationUsingKeyFrames)base.Clone(); }
protected override Freezable CreateInstanceCore() { return new GridLengthAnimationUsingKeyFrames(); }
#endregion
}
public class DiscreteGridLengthKeyFrame : DiscreteKeyFrame<GridLength> {}
public class EasingGridLengthKeyFrame : EasingKeyFrame<GridLength, GridLengthAnimationHelper> {}
public class LinearGridLengthKeyFrame : LinearKeyFrame<GridLength, GridLengthAnimationHelper> {}
public class SplineGridLengthKeyFrame : SplineKeyFrame<GridLength, GridLengthAnimationHelper> {}
Usage Example
<Storyboard>
<Animations:GridLengthAnimation
Storyboard.TargetName="GridColumn1"
Storyboard.TargetProperty="Width"
To="2*" Duration="0:0:0.2" />
<Animations:GridLengthAnimationUsingKeyFrames
Storyboard.TargetName="GridColumn2"
Storyboard.TargetProperty="Width" >
<Animations:LinearGridLengthKeyFrame KeyTime="0" Value="*" />
<Animations:EasingGridLengthKeyFrame KeyTime="0:0:0.2" Value="2*" >
<Animations:EasingGridLengthKeyFrame.EasingFunction>
<BackEase EasingMode="EaseIn" />
</Animations:EasingGridLengthKeyFrame.EasingFunction>
</Animations:EasingGridLengthKeyFrame>
</Animations:GridLengthAnimationUsingKeyFrames>
</Storyboard>
Other Classes Used in this Project
Here I want to show some classes and interfaces used internally in the project. Maybe it will help in other projects.
NullableHelper
This helper solves the problem of usage reference and value types when you want to use Nullable<>
wrapper for value type in generics.
IsNotNull
and IsNull
– verifies nullable value for null
Cast<T>
– casts nullable value to value and back. User responsible to specify correct target type
AreTypesCompatible
– verifies that specified types are compatible and will be processed correctly by this helper
IsNullable
– verifies if specified type is nullable
- Some DEBUG only functions that verify conditions and throw exceptions
SingletonOf<T>
This generic class creates application-wide singleton of a specified type. The type should be public
and have a public
default (parameterless) constructor.
Instance
– property returns the single instance of the type
public static class SingletonOf<T> where T : class, new()
{
private static readonly T _instance = new T();
public static T Instance { get { return _instance; } }
}
Note: It doesn't prevent creation of another instance of the class in a different way, but guarantees that the Instance
property always returns the one instance.
History
- 1.1 - Added
LinearGradientBrush
and RadialGradientBrush
animations.
- 1.0 - Initial version.