|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
/***************************************************************************\
*
*
* A Storyboard coordinates a set of actions in a time-dependent manner. An
* example usage is to coordinate animation events such as start/stop/pause.
*
*
\***************************************************************************/
using System.Collections; // DictionaryEntry
using System.Collections.Specialized; // HybridDictionary
using System.ComponentModel; // PropertyDescriptor
using System.Reflection; // PropertyInfo
using System.Windows.Controls; // MediaElement
using System.Windows.Markup; // INameScope
using MS.Internal; // Helper
using MS.Utility; // FrugalMap
namespace System.Windows.Media.Animation
{
/// <summary>
/// A Storyboard coordinates a set of actions in a time-dependent manner.
/// </summary>
public class Storyboard : ParallelTimeline
{
static Storyboard()
{
PropertyMetadata targetPropertyMetadata = new PropertyMetadata();
targetPropertyMetadata.FreezeValueCallback = TargetFreezeValueCallback;
TargetProperty = DependencyProperty.RegisterAttached("Target", typeof(DependencyObject), typeof(Storyboard), targetPropertyMetadata);
}
/// <summary>
/// Creates an instance of the Storyboard object.
/// </summary>
public Storyboard()
: base()
{
}
#region Freezable Requirements
/// <summary>
/// Override method required of Freezable-derived types
/// </summary>
protected override Freezable CreateInstanceCore()
{
return new Storyboard();
}
// We don't need to override CopyCore since it doesn't do anything, the
// base class will handle what is necessary.
/// <summary>
/// Override method required of Freezable-derived types
/// </summary>
public new Storyboard Clone()
{
return (Storyboard)base.Clone();
}
#endregion
#region Attached Properties
/// <summary>
/// The Target property is designed to be attached to animation
/// timelines to indicate the object they should target.
/// </summary>
public static readonly DependencyProperty TargetProperty;
/// <summary>
/// Sets value of the Target property on the specified object.
/// </summary>
public static void SetTarget(DependencyObject element, DependencyObject value)
{
ArgumentNullException.ThrowIfNull(element);
element.SetValue(TargetProperty, value);
}
/// <summary>
/// Gets the value of the Target property from the specified object.
/// </summary>
/// <remarks>
/// The target property is not serializable, since it can be set to
/// any DependencyObject, and it is not guaranteed that this object
/// can be correctly referenced from XAML.
/// </remarks>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public static DependencyObject GetTarget(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (DependencyObject)element.GetValue(TargetProperty);
}
private static bool TargetFreezeValueCallback(
DependencyObject d,
DependencyProperty dp,
EntryIndex entryIndex,
PropertyMetadata metadata,
bool isChecking)
{
// We allow the object to which the Target property is attached to be
// frozen, even though the value of the Target property is not usable
// from other threads. Clocks clone & freeze copies of their original
// timelines because the clocks will not respond to changes to those
// timelines.
return true;
}
/// <summary>
/// The TargetName property is designed to be attached to animation objects,
/// giving a string that will be matched against an element with the given name.
/// </summary>
public static readonly DependencyProperty TargetNameProperty =
DependencyProperty.RegisterAttached("TargetName", typeof(string), typeof(Storyboard));
// The static setter/getter methods for the TargetName property is required
// for parser support.
/// <summary>
/// Attaches the TargetName value on the given object.
/// </summary>
public static void SetTargetName(DependencyObject element, String name)
{
ArgumentNullException.ThrowIfNull(element);
ArgumentNullException.ThrowIfNull(name);
element.SetValue(TargetNameProperty, name);
}
/// <summary>
/// Retrieves the attached TargetName value of the given object.
/// </summary>
public static string GetTargetName(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (string)element.GetValue(TargetNameProperty);
}
/// <summary>
/// The TargetProperty property is designed to be attached to animation objects,
/// giving the string representation of the DependencyProperty that the
/// animation object will be manipulating.
/// </summary>
public static readonly DependencyProperty TargetPropertyProperty =
DependencyProperty.RegisterAttached("TargetProperty", typeof(PropertyPath), typeof(Storyboard));
// The static setter/getter methods for the TargetProperty property is required
// for parser support.
/// <summary>
/// Attaches the TargetProperty value on the given object.
/// </summary>
public static void SetTargetProperty(DependencyObject element, PropertyPath path)
{
ArgumentNullException.ThrowIfNull(element);
ArgumentNullException.ThrowIfNull(path);
element.SetValue(TargetPropertyProperty, path);
}
/// <summary>
/// Retrieves the attached TargetProperty value of the given object.
/// </summary>
public static PropertyPath GetTargetProperty(DependencyObject element)
{
ArgumentNullException.ThrowIfNull(element);
return (PropertyPath)element.GetValue(TargetPropertyProperty);
}
#endregion
/// <summary>
/// An object that represents a DependencyObject+DependencyProperty
/// pairing, designed to be used as a key into a Hashtable or similar data
/// structure.
/// </summary>
private class ObjectPropertyPair
{
public ObjectPropertyPair(DependencyObject o, DependencyProperty p)
{
_object = o;
_property = p;
}
public override int GetHashCode()
{
return _object.GetHashCode() ^ _property.GetHashCode();
}
public override bool Equals(object o)
{
if ((o != null) && (o is ObjectPropertyPair))
{
return Equals((ObjectPropertyPair)o);
}
else
{
return false;
}
}
public bool Equals(ObjectPropertyPair key)
{
return (_object.Equals(key._object) && (_property == key._property));
}
public DependencyObject DependencyObject { get { return _object; } }
public DependencyProperty DependencyProperty { get { return _property; } }
private DependencyObject _object;
private DependencyProperty _property;
}
/// <summary>
/// Finds the target element of a Storyboard.TargetName property.
/// </summary>
/// <remarks>
/// This is using a different set of FindName rules than that used
/// by ResolveBeginStoryboardName for finding a BeginStoryboard object due
/// to the different FindName behavior in templated objects.
///
/// The templated object name is the name attached to the
/// FrameworkElementFactory that created the object. There are many of them
/// created, one per templated object. So we need to use Template.FindName()
/// to find the templated child using the context of the templated parent.
///
/// Note that this FindName() function on the template class is
/// completely different from the INameScope.FindName() function on the
/// same class
/// </remarks>
internal static DependencyObject ResolveTargetName(
string targetName,
INameScope nameScope,
DependencyObject element )
{
object nameScopeUsed = null;
object namedObject = null;
DependencyObject targetObject = null;
FrameworkElement fe = element as FrameworkElement;
FrameworkContentElement fce = element as FrameworkContentElement;
if( fe != null )
{
if( nameScope != null )
{
namedObject = ((FrameworkTemplate)nameScope).FindName(targetName, fe);
nameScopeUsed = nameScope;
}
else
{
namedObject = fe.FindName(targetName);
nameScopeUsed = fe;
}
}
else if( fce != null )
{
Debug.Assert( nameScope == null );
namedObject = fce.FindName(targetName);
nameScopeUsed = fce;
}
else
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NoNameScope, targetName));
}
if( namedObject == null )
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NameNotFound, targetName, nameScopeUsed.GetType().ToString()));
}
targetObject = namedObject as DependencyObject;
if( targetObject == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_TargetNameNotDependencyObject, targetName ));
}
return targetObject;
}
/// <summary>
/// Finds a BeginStoryboard with the given name, following the rules
/// governing Storyboard. Returns null if not found.
/// </summary>
/// <remarks>
/// If a name scope is given, look there and nowhere else. In the
/// absense of name scope, use Framework(Content)Element.FindName which
/// has its own complex set of rules for looking up name scopes.
///
/// This is a different set of rules than from that used to look up
/// the TargetName. BeginStoryboard name is registered with the template
/// INameScope on a per-template basis. So we look it up using
/// INameScope.FindName(). This is a function completely different from
/// Template.FindName().
/// </remarks>
internal static BeginStoryboard ResolveBeginStoryboardName(
string targetName,
INameScope nameScope,
FrameworkElement fe,
FrameworkContentElement fce)
{
object namedObject = null;
BeginStoryboard beginStoryboard = null;
if( nameScope != null )
{
namedObject = nameScope.FindName(targetName);
if( namedObject == null )
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NameNotFound, targetName, nameScope.GetType().ToString()));
}
}
else if( fe != null )
{
namedObject = fe.FindName(targetName);
if( namedObject == null )
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NameNotFound, targetName, fe.GetType().ToString()));
}
}
else if( fce != null )
{
namedObject = fce.FindName(targetName);
if( namedObject == null )
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NameNotFound, targetName, fce.GetType().ToString()));
}
}
else
{
throw new InvalidOperationException(
SR.Format(SR.Storyboard_NoNameScope, targetName));
}
beginStoryboard = namedObject as BeginStoryboard;
if( beginStoryboard == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_BeginStoryboardNameNotFound, targetName));
}
return beginStoryboard;
}
/// <summary>
/// Recursively walks the clock tree and determine the target object
/// and property for each clock in the tree.
/// </summary>
/// <remarks>
/// The currently active object and property path are passed in as parameters,
/// they will be used unless a target/property specification exists on
/// the Timeline object corresponding to the current clock. (So that the
/// leaf-most reference wins.)
///
/// The active object and property parameters may be null if they have
/// never been specified. If we reach a leaf node clock and a needed attribute
/// is still null, it is an error condition. Otherwise we keep hoping they'll be found.
/// </remarks>
private void ClockTreeWalkRecursive(
Clock currentClock, /* No two calls will have the same currentClock */
DependencyObject containingObject, /* Remains the same through all the recursive calls */
INameScope nameScope, /* Remains the same through all the recursive calls */
DependencyObject parentObject,
string parentObjectName,
PropertyPath parentPropertyPath,
HandoffBehavior handoffBehavior, /* Remains the same through all the recursive calls */
HybridDictionary clockMappings,
Int64 layer /* Remains the same through all the recursive calls */)
{
Timeline currentTimeline = currentClock.Timeline;
DependencyObject targetObject = parentObject;
string currentObjectName = parentObjectName;
PropertyPath currentPropertyPath = parentPropertyPath;
// If we have target object/property information, use it instead of the
// parent's information.
string nameString = (string)currentTimeline.GetValue(TargetNameProperty);
if( nameString != null )
{
if( nameScope is Style )
{
// We are inside a Style - we don't let people target anything.
// They're only allowed to modify the Styled object, which is
// already the implicit target.
throw new InvalidOperationException(SR.Format(SR.Storyboard_TargetNameNotAllowedInStyle, nameString));
}
currentObjectName = nameString;
}
// The TargetProperty trumps the TargetName property.
DependencyObject localTargetObject = (DependencyObject) currentTimeline.GetValue(TargetProperty);
if( localTargetObject != null )
{
targetObject = localTargetObject;
currentObjectName = null;
}
PropertyPath propertyPath = (PropertyPath)currentTimeline.GetValue(TargetPropertyProperty);
if( propertyPath != null )
{
currentPropertyPath = propertyPath;
}
// Now see if the current clock is an animation clock
if( currentClock is AnimationClock )
{
DependencyProperty targetProperty = null;
AnimationClock animationClock = (AnimationClock)currentClock;
if( targetObject == null )
{
// Resolve the target object name. If no name specified, use the
// containing object.
if( currentObjectName != null )
{
DependencyObject mentor = Helper.FindMentor(containingObject);
targetObject = ResolveTargetName(currentObjectName, nameScope, mentor);
}
else
{
// The containing object must be either an FE or FCE.
// (Not a Storyboard, as used for "shared clocks" mode.)
targetObject = containingObject as FrameworkElement;
if(targetObject == null)
{
targetObject = containingObject as FrameworkContentElement;
}
if( targetObject == null )
{
// The containing object is not an FE or FCE.
throw new InvalidOperationException(SR.Format(SR.Storyboard_NoTarget, currentTimeline.GetType().ToString() ));
}
}
}
// See if we have a property name to use.
if( currentPropertyPath == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_TargetPropertyRequired, currentTimeline.GetType().ToString() ));
}
// A property name can be a straightforward property name (like "Angle")
// but may be a more complex multi-step property path. The two cases
// are handled differently.
using(currentPropertyPath.SetContext(targetObject))
{
if( currentPropertyPath.Length < 1 )
{
throw new InvalidOperationException(SR.Storyboard_PropertyPathEmpty);
}
VerifyPathIsAnimatable(currentPropertyPath);
if( currentPropertyPath.Length == 1 )
{
// We have a simple single-step property.
targetProperty = currentPropertyPath.GetAccessor(0) as DependencyProperty;
if( targetProperty == null )
{
// Unfortunately it's not a DependencyProperty.
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathMustPointToDependencyProperty, currentPropertyPath.Path ));
}
VerifyAnimationIsValid(targetProperty, animationClock);
ObjectPropertyPair animatedTarget = new ObjectPropertyPair(targetObject, targetProperty);
UpdateMappings(clockMappings, animatedTarget, animationClock);
}
else // path.Length > 1
{
// This is a multi-step property path that requires more extensive
// setup.
ProcessComplexPath(clockMappings, targetObject, currentPropertyPath, animationClock, handoffBehavior, layer);
}
}
}
else if ( currentClock is MediaClock ) // Not an animation clock - maybe a media clock?
{
// Yes it's a media clock. Try to find the corresponding object and
// apply the clock to that object.
ApplyMediaClock(nameScope, containingObject, targetObject, currentObjectName, (MediaClock) currentClock);
}
else
{
// None of the types we recognize as leaf node clock types -
// recursively process child clocks.
ClockGroup currentClockGroup = currentClock as ClockGroup;
if (currentClockGroup != null)
{
ClockCollection childrenClocks = currentClockGroup.Children;
for( int i = 0; i < childrenClocks.Count; i++ )
{
ClockTreeWalkRecursive(
childrenClocks[i],
containingObject,
nameScope,
targetObject,
currentObjectName,
currentPropertyPath,
handoffBehavior,
clockMappings,
layer);
}
}
}
}
/// <summary>
/// When we've found a media clock, try to find a corresponding media
/// element and attach the media clock to that element.
/// </summary>
private static void ApplyMediaClock( INameScope nameScope, DependencyObject containingObject,
DependencyObject currentObject, string currentObjectName, MediaClock mediaClock )
{
MediaElement targetMediaElement = null;
if( currentObjectName != null )
{
// Find the object named as the current target name.
DependencyObject mentor = Helper.FindMentor(containingObject);
targetMediaElement = ResolveTargetName(currentObjectName, nameScope, mentor ) as MediaElement;
if( targetMediaElement == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_MediaElementNotFound, currentObjectName ));
}
}
else if( currentObject != null )
{
targetMediaElement = currentObject as MediaElement;
}
else
{
targetMediaElement = containingObject as MediaElement;
}
if( targetMediaElement == null )
{
throw new InvalidOperationException(SR.Storyboard_MediaElementRequired);
}
targetMediaElement.Clock = mediaClock;
}
/// <summary>
/// Given an animation clock, add it to the data structure which tracks
/// all the clocks along with their associated target object and property.
/// </summary>
private static void UpdateMappings(
HybridDictionary clockMappings,
ObjectPropertyPair mappingKey,
AnimationClock animationClock)
{
object mappedObject = clockMappings[mappingKey];
Debug.Assert( mappedObject == null || mappedObject is AnimationClock || mappedObject is List<AnimationClock>,
$"Internal error - clockMappings table contains an unexpected object {((mappedObject == null) ? "" : mappedObject.GetType().ToString())}");
if( mappedObject == null )
{
// No clock currently in storage, put this clock in that slot.
clockMappings[mappingKey] = animationClock;
}
else if( mappedObject is AnimationClock )
{
// One clock currently in storage, up-convert to list and replace in slot.
List<AnimationClock> clockList = new List<AnimationClock>();
clockList.Add((AnimationClock)mappedObject);
clockList.Add(animationClock);
clockMappings[mappingKey] = clockList;
}
else // mappedObject is List<AnimationClock>
{
// Add to existing list in storage.
List<AnimationClock> clockList = (List<AnimationClock>)mappedObject;
clockList.Add(animationClock);
}
return;
}
/// <summary>
/// Takes the built up mapping table for animation clocks and applies
/// them to the specified property on the specified object.
/// </summary>
private static void ApplyAnimationClocks( HybridDictionary clockMappings, HandoffBehavior handoffBehavior, Int64 layer )
{
foreach( DictionaryEntry entry in clockMappings )
{
ObjectPropertyPair key = (ObjectPropertyPair)entry.Key;
object value = entry.Value;
List<AnimationClock> clockList;
Debug.Assert( value is AnimationClock || value is List<AnimationClock> ,
$"Internal error - clockMappings table contains unexpected object of type{value.GetType()}");
if( value is AnimationClock )
{
clockList = new List<AnimationClock>(1);
clockList.Add((AnimationClock)value);
}
else // if( value is List<AnimationClock> )
{
clockList = (List<AnimationClock>)value;
}
AnimationStorage.ApplyAnimationClocksToLayer(
key.DependencyObject,
key.DependencyProperty,
clockList,
handoffBehavior,
layer);
}
}
/// <summary>
/// Function that checks to see if a given PropertyPath (already given
/// the context object) can be used.
/// </summary>
// The rules currently in effect:
// * The last object in the path must be a DependencyObject
// * The last property (on that last object) must be a DependencyProperty
// * Any of these objects may be Freezable objects. There are two cases for
// this.
// 1) The value of the first property is Frozen. We might be able to
// handle this via the cloning mechanism, so we don't check Frozen-ness
// if we see the first property is Frozen. Whether the cloning
// mechanism can be used is verified elsewhere.
// 2) The value of the first property is not Frozen, or is not a Freezable
// at all. In this case, the cloning code path does not apply, and
// thus we must not have any immutable Freezable objects any further
// down the line.
// Another rule not enforced here:
// * If cloning is required, the first property value must be a Freezable,
// which knows how to clone itself. However, this is only needed in
// cases of complex property paths and is checked elsewhere.
// Things we don't care about:
// * Whether or not any of the intermediate objects are DependencyObject or
// not - this is supposed to work no matter the object type.
// * Whether or not any of the intermediate properties are DP or not - this
// is supposed to work whether it's a CLR property or DependencyProperty.
// * Whether or not any of the intermediate properties are animatable or not.
// Even though they are changing, we're not attaching an animation to clock
// to those properties specifically.
// * By the same token, we don't care if any of them are marked Read-Only.
// Note that this means: If the intention is to make something fixed, it is
// not sufficient to mark an intermediate property read-only and
// not-animatable. In fact, in the current design, it is impossible to
// be 100% sure that something will stay put.
internal static void VerifyPathIsAnimatable(PropertyPath path)
{
object intermediateObject = null;
object intermediateProperty = null; // Might be DependencyProperty, PropertyInfo, or PropertyDescriptor
bool checkingFrozenState = true;
Freezable intermediateFreezable = null;
for( int i=0; i < path.Length; i++ )
{
intermediateObject = path.GetItem(i);
intermediateProperty = path.GetAccessor(i);
if( intermediateObject == null )
{
Debug.Assert( i > 0, "The caller should not have set the PropertyPath context to a null object." );
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathObjectNotFound, AccessorName(path, i-1), path.Path ));
}
if( intermediateProperty == null )
{
// Would love to throw error with the name of the property we couldn't find,
// but that information is not exposed from the PropertyPath class.
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathPropertyNotFound, path.Path ));
}
// If the first property value is an immutable Freezable, then turn
// off the Frozen state checking - let's hope we can use the cloning
// mechanism for that case.
// Index of zero is the path context object itself, one (that we're
// checking here) is the value of the first property.
// Example: Property path "Background.Opacity" as applied to Button.
// Object 0 is the Button, object 1 is the brush.
if( i == 1 )
{
intermediateFreezable = intermediateObject as Freezable;
if( intermediateFreezable != null && intermediateFreezable.IsFrozen )
{
checkingFrozenState = false;
}
}
// Freezable objects (other than the one returned as the value of
// the first property) must not be frozen if the first one isn't.
else if( checkingFrozenState )
{
intermediateFreezable = intermediateObject as Freezable;
if( intermediateFreezable != null && intermediateFreezable.IsFrozen )
{
if( i > 0 )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathFrozenCheckFailed, AccessorName(path, i-1), path.Path, intermediateFreezable.GetType().ToString() ));
}
else
{
// i == 0 means the targeted object itself is a frozen Freezable.
// This need a different error message.
throw new InvalidOperationException(SR.Format(SR.Storyboard_ImmutableTargetNotSupported, path.Path));
}
}
}
// The last object + property pairing (the one we're actually going
// to stick the clock on) has further requirements.
if( i == path.Length-1 )
{
DependencyObject intermediateDO = intermediateObject as DependencyObject;
DependencyProperty intermediateDP = intermediateProperty as DependencyProperty;
if( intermediateDO == null )
{
Debug.Assert( i > 0, "The caller should not have set the PropertyPath context to a non DependencyObject." );
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathMustPointToDependencyObject, AccessorName(path, i-1), path.Path));
}
if( intermediateDP == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathMustPointToDependencyProperty, path.Path ));
}
if( checkingFrozenState && intermediateDO.IsSealed )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathSealedCheckFailed, intermediateDP.Name, path.Path, intermediateDO));
}
if(!AnimationStorage.IsPropertyAnimatable(intermediateDO, intermediateDP) )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathIncludesNonAnimatableProperty, path.Path, intermediateDP.Name));
}
}
}
}
private static string AccessorName( PropertyPath path, int index )
{
object propertyAccessor = path.GetAccessor(index);
if( propertyAccessor is DependencyProperty )
{
return ((DependencyProperty)propertyAccessor).Name;
}
else if( propertyAccessor is PropertyInfo )
{
return ((PropertyInfo)propertyAccessor).Name;
}
else if( propertyAccessor is PropertyDescriptor )
{
return ((PropertyDescriptor)propertyAccessor).Name;
}
else
{
return "[Unknown]";
}
}
/// <summary>
/// Makes sure that the given clock can animate the given property -
/// throw an exception otherwise.
/// </summary>
private static void VerifyAnimationIsValid( DependencyProperty targetProperty, AnimationClock animationClock )
{
if( !AnimationStorage.IsAnimationClockValid(targetProperty, animationClock) )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_AnimationMismatch, animationClock.Timeline.GetType(), targetProperty.Name, targetProperty.PropertyType));
}
}
/// <summary>
/// For complex property paths, we need to dig our way down to the
/// property and attach the animation clock there. We will not be able to
/// actually attach the clocks if the targetProperty points to a frozen
/// Freezable. More extensive handling will be required for that case.
/// </summary>
private void ProcessComplexPath( HybridDictionary clockMappings, DependencyObject targetObject,
PropertyPath path, AnimationClock animationClock, HandoffBehavior handoffBehavior, Int64 layer )
{
Debug.Assert(path.Length > 1, "This method shouldn't even be called for a simple property path.");
// For complex paths, the target object/property differs from the actual
// animated object/property.
//
// Example:
// TargetName="Rect1" TargetProperty="(Rectangle.LayoutTransform).(RotateTransform.Angle)"
//
// The target object is a Rectangle.
// The target property is LayoutTransform.
// The animated object is a RotateTransform
// The animated property is Angle.
// Currently unsolved problem: If the LayoutTransform is not a RotateTransform,
// we have no way of knowing. We'll merrily set up to animate the Angle
// property as an attached property, not knowing that the value will be
// completely ignored.
DependencyProperty targetProperty = path.GetAccessor(0) as DependencyProperty;
// Two different ways to deal with property paths. If the target is
// on a frozen Freezable, we'll have to make a clone of the value and
// attach the animation on the clone instead.
// For all other objects, we attach the animation clock directly on the
// specified animating object and property.
object targetPropertyValue = targetObject.GetValue(targetProperty);
DependencyObject animatedObject = path.LastItem as DependencyObject;
DependencyProperty animatedProperty = path.LastAccessor as DependencyProperty;
if( animatedObject == null ||
animatedProperty == null ||
targetProperty == null )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_PropertyPathUnresolved, path.Path));
}
VerifyAnimationIsValid(animatedProperty, animationClock);
if( PropertyCloningRequired( targetPropertyValue ) )
{
// Verify that property paths are supported for the specified
// object and property. If the property value query (usually in
// GetValueCore) doesn't call into Storyboard code, then none of this
// will have any effect. (Silently do nothing.)
// Throwing here is for user's sake to alert that nothing will happen.
VerifyComplexPathSupport( targetObject );
// We need to clone the value of the target, and from here onwards
// try to pretend that it is the actual value.
Debug.Assert(targetPropertyValue is Freezable, "We shouldn't be trying to clone a type we don't understand. PropertyCloningRequired() has improperly flagged the current value as 'need to clone'.");
// To enable animations on frozen Freezable objects, complex
// path processing is done on a clone of the value.
Freezable clone = ((Freezable)targetPropertyValue).Clone();
SetComplexPathClone( targetObject, targetProperty, targetPropertyValue, clone );
// Promote the clone to the EffectiveValues cache
targetObject.InvalidateProperty(targetProperty);
// We're supposed to have the animatable clone in place by now. But if
// things went sour for whatever reason, halt the app instead of corrupting
// the frozen object.
if( targetObject.GetValue(targetProperty) != clone )
{
throw new InvalidOperationException(SR.Format(SR.Storyboard_ImmutableTargetNotSupported, path.Path));
}
// Now that we have a clone, update the animatedObject and animatedProperty
// with references to those on the clone.
using(path.SetContext(targetObject))
{
animatedObject = path.LastItem as DependencyObject;
animatedProperty = path.LastAccessor as DependencyProperty;
}
// And set up to listen to changes on this clone.
ChangeListener.ListenToChangesOnFreezable(
targetObject, clone, targetProperty, (Freezable)targetPropertyValue );
}
// Apply animation clock on the animated object/animated property.
ObjectPropertyPair directApplyTarget = new ObjectPropertyPair( animatedObject, animatedProperty );
UpdateMappings( clockMappings, directApplyTarget, animationClock );
}
private bool PropertyCloningRequired( object targetPropertyValue )
{
bool cloningRequired;
if( targetPropertyValue is Freezable )
{
if( ((Freezable)targetPropertyValue).IsFrozen )
{
// The target property value is a Freezable, and is frozen.
// we will need to clone in order to use a complex property path.
cloningRequired = true;
}
else
{
// The target property value is a Freezable, and is not frozen.
// We can apply the animation clocks directly.
cloningRequired = false;
}
}
else
{
// We have no idea what this might be and can't tell if we need to
// clone it. But even if we do, we don't know how, so we won't.
cloningRequired = false;
}
return cloningRequired;
}
/// <summary>
/// Check to see if the given object and property combination will be
/// able to resolve complex paths.
/// </summary>
private void VerifyComplexPathSupport( DependencyObject targetObject )
{
if(targetObject is FrameworkElement)
{
// FrameworkElement and derived types are supported.
return;
}
if(targetObject is FrameworkContentElement)
{
// FrameworkContentElement and derived types are supported.
return;
}
// ... and anything else that knows to call into Storyboard.GetComplexPathValue.
// Otherwise - throw.
throw new InvalidOperationException(SR.Format(SR.Storyboard_ComplexPathNotSupported, targetObject.GetType().ToString()));
}
/// <summary>
/// Check to see if there is a complex path that started with the
/// given target object and property. If so, process the complex path
/// information and return the results.
/// </summary>
internal static void GetComplexPathValue(
DependencyObject targetObject,
DependencyProperty targetProperty,
ref EffectiveValueEntry entry,
PropertyMetadata metadata)
{
CloneCacheEntry cacheEntry = GetComplexPathClone(targetObject, targetProperty);
if (cacheEntry != null)
{
object baseValue = entry.Value;
if (baseValue == DependencyProperty.UnsetValue)
{
// If the incoming baseValue is DependencyProperty.UnsetValue, that
// means the current property value is the default value. Either
// the cacheEntry.Clone was a clone of a default value (and should be
// returned to the caller) or someone called ClearValue() (and
// cacheEntry.Clone should be cleared accordingly).
// To distinguish these cases we must check the cached source
// against the default value.
//
// We don't have to handle the ClearValue case in this clause;
// the comparison with the cached source to the base value
// will fail in that case (since the cached source won't be UnsetValue)
// and we'll clear out the cache.
Debug.Assert(cacheEntry.Source != DependencyProperty.UnsetValue,
"Storyboard complex path\u2019s clone cache should never contain DependencyProperty.UnsetValue. Either something went wrong in Storyboard.ProcessComplexPath() or somebody else is messing with the Storyboard clone cache.");
if (cacheEntry.Source == metadata.GetDefaultValue(targetObject, targetProperty))
{
// The cacheEntry.Clone is the clone of the default value. In normal
// non-Storyboard code paths, BaseValueSourceInternal is Unknown for default
// values at this time, so we need to switch it over explicitly.
//
// And to prevent DependencyObject.UpdateEffectiveValue from misconstruing this
// as an unaltered default value (which would result in UEV thinking no change
// in value occurred and discarding this new value), we will go ahead and set the
// animated value modifier on this value entry. (jeffbog: B#1616678 5/19/2006)
//
// In all other cases, valueSource should have the correct
// valueSource corresponding to the object we cloned from,
// so we don't need to do anything.
entry.BaseValueSourceInternal = BaseValueSourceInternal.Default;
entry.SetAnimatedValue(cacheEntry.Clone, DependencyProperty.UnsetValue);
return;
}
}
// If the incoming baseValue is a deferred object, we need to get the
// real value to make a valid comparison against the cache entry source.
DeferredReference deferredBaseValue = baseValue as DeferredReference;
if (deferredBaseValue != null)
{
baseValue = deferredBaseValue.GetValue(entry.BaseValueSourceInternal);
entry.Value = baseValue;
}
// If the incoming baseValue is different from the original source object that
// we cloned and cached then we need to invalidate this cache. Otherwise we use
// the value in the cache as is.
if (cacheEntry.Source == baseValue)
{
CloneEffectiveValue(ref entry, cacheEntry);
return;
}
else
{
// Setting to DependencyProperty.UnsetValue is how FrugalMap does delete.
SetComplexPathClone(
targetObject,
targetProperty,
DependencyProperty.UnsetValue,
DependencyProperty.UnsetValue);
}
}
}
private static void CloneEffectiveValue(ref EffectiveValueEntry entry, CloneCacheEntry cacheEntry)
{
object clonedValue = cacheEntry.Clone;
/*
if (!entry.IsExpression)
{
if (entry.LocalValue != clonedValue)
{
entry.Value = clonedValue;
}
}
else
{
ModifiedValue modifiedValue = entry.ModifiedValue;
if (modifiedValue.ExpressionValue != clonedValue)
{
modifiedValue.ExpressionValue = clonedValue;
}
}
*/
if (!entry.IsExpression)
{
entry.Value = clonedValue;
}
else
{
entry.ModifiedValue.ExpressionValue = clonedValue;
}
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkElement containingObject )
{
Begin( containingObject, HandoffBehavior.SnapshotAndReplace, false );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkElement containingObject, HandoffBehavior handoffBehavior )
{
Begin( containingObject, handoffBehavior, false );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkElement containingObject, bool isControllable )
{
Begin(containingObject, HandoffBehavior.SnapshotAndReplace, isControllable );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkElement containingObject, HandoffBehavior handoffBehavior, bool isControllable )
{
BeginCommon(containingObject, null, handoffBehavior, isControllable, Storyboard.Layers.Code );
}
/// <summary>
/// Begins all ControlTemplate animations underneath this storyboard, clock tree starts at the given Control.
/// </summary>
public void Begin( FrameworkElement containingObject, FrameworkTemplate frameworkTemplate )
{
Begin( containingObject, frameworkTemplate, HandoffBehavior.SnapshotAndReplace, false );
}
/// <summary>
/// Begins all ControlTemplate animations underneath this storyboard, clock tree starts at the given Control.
/// </summary>
public void Begin( FrameworkElement containingObject, FrameworkTemplate frameworkTemplate, HandoffBehavior handoffBehavior )
{
Begin( containingObject, frameworkTemplate, handoffBehavior, false );
}
/// <summary>
/// Begins all ControlTemplate animations underneath this storyboard, clock tree starts at the given Control.
/// </summary>
public void Begin( FrameworkElement containingObject, FrameworkTemplate frameworkTemplate, bool isControllable )
{
Begin(containingObject, frameworkTemplate, HandoffBehavior.SnapshotAndReplace, isControllable );
}
/// <summary>
/// Begins all ControlTemplate animations underneath this storyboard, clock tree starts at the given Control.
/// </summary>
public void Begin( FrameworkElement containingObject, FrameworkTemplate frameworkTemplate, HandoffBehavior handoffBehavior, bool isControllable )
{
BeginCommon(containingObject, frameworkTemplate, handoffBehavior, isControllable, Storyboard.Layers.Code );
}
/* This is the ContentControl+DataTemplate counterpert to Control+ControlTemplate above, need test signoff before enabling.
/// <summary>
/// Begins all DataTemplate animations underneath this storyboard, clock tree starts at the given ContentControl.
/// </summary>
public void Begin( ContentControl contentControl, DataTemplate dataTemplate )
{
Begin( contentControl, dataTemplate, HandoffBehavior.SnapshotAndReplace, false );
}
/// <summary>
/// Begins all DataTemplate animations underneath this storyboard, clock tree starts at the given ContentControl.
/// </summary>
public void Begin( ContentControl contentControl, DataTemplate dataTemplate, HandoffBehavior handoffBehavior )
{
Begin( contentControl, dataTemplate, handoffBehavior, false );
}
/// <summary>
/// Begins all DataTemplate animations underneath this storyboard, clock tree starts at the given ContentControl.
/// </summary>
public void Begin( ContentControl contentControl, DataTemplate dataTemplate, bool isControllable )
{
Begin( contentControl, dataTemplate, HandoffBehavior.SnapshotAndReplace, isControllable );
}
/// <summary>
/// Begins all DataTemplate animations underneath this storyboard, clock tree starts at the given ContentControl.
/// </summary>
public void Begin( ContentControl contentControl, DataTemplate dataTemplate, HandoffBehavior handoffBehavior, bool isControllable )
{
BeginCommon( contentControl, dataTemplate, handoffBehavior, isControllable, Storyboard.Layers.Code );
}
*/
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkContentElement containingObject )
{
Begin( containingObject, HandoffBehavior.SnapshotAndReplace, false );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkContentElement containingObject, HandoffBehavior handoffBehavior )
{
Begin( containingObject, handoffBehavior, false );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkContentElement containingObject, bool isControllable )
{
Begin(containingObject, HandoffBehavior.SnapshotAndReplace, isControllable );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
public void Begin( FrameworkContentElement containingObject, HandoffBehavior handoffBehavior, bool isControllable )
{
BeginCommon(containingObject, null, handoffBehavior, isControllable, Storyboard.Layers.Code );
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts in "shared clocks" mode.
/// </summary>
public void Begin()
{
DependencyObject containingObject = this; // Helper.FindMentor(this);
INameScope nameScope = null;
HandoffBehavior handoffBehavior = HandoffBehavior.SnapshotAndReplace;
bool isControllable = true;
Int64 layer = Storyboard.Layers.Code;
BeginCommon(containingObject, nameScope, handoffBehavior, isControllable, layer);
}
/// <summary>
/// Begins all animations underneath this storyboard, clock tree starts at the given containing object.
/// </summary>
internal void BeginCommon( DependencyObject containingObject, INameScope nameScope,
HandoffBehavior handoffBehavior, bool isControllable, Int64 layer)
{
ArgumentNullException.ThrowIfNull(containingObject);
if (!HandoffBehaviorEnum.IsDefined(handoffBehavior))
{
throw new ArgumentException(SR.Storyboard_UnrecognizedHandoffBehavior);
}
if (BeginTime == null)
{
// a null BeginTime means to not allocate or start the clock
return;
}
// It's not possible to begin when there is no TimeManager. This condition
// is known to occur during app shutdown. Since an app being shut down
// won't care about its Storyboards, we silently exit.
// If we don't exit here, we'll need to catch and handle the "no time
// manager" exception implemented for bug #1247862
if( MediaContext.CurrentMediaContext.TimeManager == null )
{
return;
}
if( TraceAnimation.IsEnabled )
{
TraceAnimation.TraceActivityItem(
TraceAnimation.StoryboardBegin,
this,
Name,
containingObject,
nameScope );
}
// This table maps an [object,property] key pair to one or more clocks.
// If we have knowledge of whether this Storyboard was changed between multiple
// Begin(), we can cache this. But we don't know, so we don't cache.
HybridDictionary simplePathClockMappings = new HybridDictionary();
// Create (and Begin) a clock tree corresponding to this Storyboard timeline tree
Clock storyboardClockTree = CreateClock(isControllable);
// We now have one or more clocks that are created from this storyboard.
// However, the individual clocks are not necessarily intended for
// the containing object so we need to do a tree walk and sort out
// which clocks go on which objects and their properties.
ClockTreeWalkRecursive(
storyboardClockTree,
containingObject,
nameScope,
null, /* target object */
null, /* target object name */
null, /* target property path */
handoffBehavior,
simplePathClockMappings,
layer);
// Apply the storyboard's animation clocks we found in the tree walk
ApplyAnimationClocks( simplePathClockMappings, handoffBehavior, layer );
if (isControllable)
{
// Save a reference to this clock tree on the containingObject. We
// need it there in order to control this clock tree later.
SetStoryboardClock(containingObject, storyboardClockTree);
}
return;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current global speed.
/// </summary>
public Nullable<Double> GetCurrentGlobalSpeed( FrameworkElement containingObject )
{
return GetCurrentGlobalSpeedImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current global speed.
/// </summary>
public Nullable<Double> GetCurrentGlobalSpeed( FrameworkContentElement containingObject )
{
return GetCurrentGlobalSpeedImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, return the current global speed.
/// </summary>
public Double GetCurrentGlobalSpeed()
{
Nullable<Double> currentGlobalSpeed = GetCurrentGlobalSpeedImpl(this);
if(currentGlobalSpeed.HasValue)
{
return currentGlobalSpeed.Value;
}
else
{
return default(Double);
}
}
private Nullable<Double> GetCurrentGlobalSpeedImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.CurrentGlobalSpeed;
}
return null;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current iteration.
/// </summary>
public Nullable<Int32> GetCurrentIteration( FrameworkElement containingObject )
{
return GetCurrentIterationImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current iteration.
/// </summary>
public Nullable<Int32> GetCurrentIteration( FrameworkContentElement containingObject )
{
return GetCurrentIterationImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, return the current iteration.
/// </summary>
public Int32 GetCurrentIteration()
{
Nullable<Int32> currentIteration = GetCurrentIterationImpl(this);
if(currentIteration.HasValue)
{
return currentIteration.Value;
}
else
{
return default(Int32);
}
}
private Nullable<Int32> GetCurrentIterationImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.CurrentIteration;
}
return null;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current progress.
/// </summary>
public Nullable<Double> GetCurrentProgress( FrameworkElement containingObject )
{
return GetCurrentProgressImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current progress.
/// </summary>
public Nullable<Double> GetCurrentProgress( FrameworkContentElement containingObject )
{
return GetCurrentProgressImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, return the current progress.
/// </summary>
public Double GetCurrentProgress()
{
Nullable<Double> currentProgress = GetCurrentProgressImpl(this);
if(currentProgress.HasValue)
{
return currentProgress.Value;
}
else
{
return default(Double);
}
}
private Nullable<Double> GetCurrentProgressImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.CurrentProgress;
}
return null;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current state.
/// </summary>
public ClockState GetCurrentState( FrameworkElement containingObject )
{
return GetCurrentStateImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current state.
/// </summary>
public ClockState GetCurrentState( FrameworkContentElement containingObject )
{
return GetCurrentStateImpl(containingObject);
}
/// <summary>
/// Return the current state of this storyboard. This storyboard
/// must have been created in "shared clocks" mode.
/// </summary>
public ClockState GetCurrentState()
{
return GetCurrentStateImpl(this);
}
private ClockState GetCurrentStateImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.CurrentState;
}
return ClockState.Stopped; // Not default(ClockState)...
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current time.
/// </summary>
public Nullable<TimeSpan> GetCurrentTime( FrameworkElement containingObject )
{
return GetCurrentTimeImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return the current time.
/// </summary>
public Nullable<TimeSpan> GetCurrentTime( FrameworkContentElement containingObject )
{
return GetCurrentTimeImpl(containingObject);
}
/// <summary>
/// Return the current time of this storyboard. This storyboard
/// must have been created in "shared clocks" mode.
/// </summary>
public TimeSpan GetCurrentTime()
{
Nullable<TimeSpan> currentTime = GetCurrentTimeImpl(this);
if(currentTime.HasValue)
{
return currentTime.Value;
}
else
{
return default(TimeSpan);
}
}
private Nullable<TimeSpan> GetCurrentTimeImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.CurrentTime;
}
return null;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return whether the clock is paused.
/// </summary>
public bool GetIsPaused( FrameworkElement containingObject )
{
return GetIsPausedImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, return whether the clock is paused.
/// </summary>
public bool GetIsPaused( FrameworkContentElement containingObject )
{
return GetIsPausedImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, return whether the clock is paused.
/// </summary>
public bool GetIsPaused()
{
return GetIsPausedImpl(this);
}
private bool GetIsPausedImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject);
if (clock != null)
{
return clock.IsPaused;
}
// A clock that has been disposed is not in a paused state.
return false;
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call pause on the clock.
/// </summary>
public void Pause( FrameworkElement containingObject )
{
PauseImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call pause on the clock.
/// </summary>
public void Pause( FrameworkContentElement containingObject )
{
PauseImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call pause on the clock.
/// </summary>
public void Pause()
{
PauseImpl(this);
}
private void PauseImpl(DependencyObject containingObject)
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.Pause);
if (clock != null)
{
clock.Controller.Pause();
}
if( TraceAnimation.IsEnabled )
{
TraceAnimation.TraceActivityItem(
TraceAnimation.StoryboardPause,
this,
Name,
this );
}
}
/// <summary>
/// If a clock was generated from 'this' storyboard for the given object, remove it.
/// </summary>
public void Remove(FrameworkElement containingObject)
{
RemoveImpl(containingObject);
}
/// <summary>
/// If a clock was generated from 'this' storyboard for the given object, remove it.
/// </summary>
public void Remove(FrameworkContentElement containingObject)
{
RemoveImpl(containingObject);
}
/// <summary>
/// If a clock was generated from this storyboard, remove it.
/// </summary>
public void Remove()
{
RemoveImpl(this);
}
private void RemoveImpl(DependencyObject containingObject)
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.Remove);
if (clock != null)
{
clock.Controller.Remove();
HybridDictionary clocks = StoryboardClockTreesField.GetValue(containingObject);
if (clocks != null)
{
clocks.Remove(this);
}
}
if( TraceAnimation.IsEnabled )
{
TraceAnimation.TraceActivityItem(
TraceAnimation.StoryboardRemove,
this,
Name,
containingObject );
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call resume on the clock.
/// </summary>
public void Resume( FrameworkElement containingObject )
{
ResumeImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call resume on the clock.
/// </summary>
public void Resume( FrameworkContentElement containingObject )
{
ResumeImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call resume on the clock.
/// </summary>
public void Resume()
{
ResumeImpl(this);
}
private void ResumeImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.Resume);
if (clock != null)
{
clock.Controller.Resume();
}
if( TraceAnimation.IsEnabled )
{
TraceAnimation.TraceActivityItem(
TraceAnimation.StoryboardResume,
this,
Name,
containingObject );
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call seek on the clock
/// with the given parameters.
/// </summary>
public void Seek( FrameworkElement containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
SeekImpl(containingObject, offset, origin);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call seek on the clock
/// with the given parameters.
/// </summary>
public void Seek( FrameworkContentElement containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
SeekImpl(containingObject, offset, origin);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call seek on the clock with the given
/// parameters.
/// </summary>
public void Seek( TimeSpan offset, TimeSeekOrigin origin )
{
SeekImpl(this, offset, origin);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call seek on the clock with the given
/// parameters.
/// </summary>
public void Seek( TimeSpan offset )
{
SeekImpl(this, offset, TimeSeekOrigin.BeginTime);
}
private void SeekImpl( DependencyObject containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.Seek);
if (clock != null)
{
// Seek is a public API as well, so its parameters should get validated there.
clock.Controller.Seek(offset, origin);
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call SeekAlignedToLastTick
/// on the clock with the given parameters.
/// </summary>
public void SeekAlignedToLastTick( FrameworkElement containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
SeekAlignedToLastTickImpl(containingObject, offset, origin);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call SeekAlignedToLastTick
/// on the clock with the given parameters.
/// </summary>
public void SeekAlignedToLastTick( FrameworkContentElement containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
SeekAlignedToLastTickImpl(containingObject, offset, origin);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call SeekAlignedToLastTick on the clock
/// with the given parameters.
/// </summary>
public void SeekAlignedToLastTick( TimeSpan offset, TimeSeekOrigin origin )
{
SeekAlignedToLastTickImpl(this, offset, origin);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call SeekAlignedToLastTick on the clock
/// with the given parameters.
/// </summary>
public void SeekAlignedToLastTick( TimeSpan offset )
{
SeekAlignedToLastTickImpl(this, offset, TimeSeekOrigin.BeginTime);
}
private void SeekAlignedToLastTickImpl( DependencyObject containingObject, TimeSpan offset, TimeSeekOrigin origin )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.SeekAlignedToLastTick);
if (clock != null)
{
// SeekAlignedToLastTick is a public API as well, so its parameters should get validated there.
clock.Controller.SeekAlignedToLastTick(offset, origin);
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, set the speed ratio on
/// the clock to the given ratio.
/// </summary>
public void SetSpeedRatio( FrameworkElement containingObject, double speedRatio )
{
SetSpeedRatioImpl(containingObject, speedRatio);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, set the speed ratio on the clock
/// with the given parameters.
/// </summary>
public void SetSpeedRatio( FrameworkContentElement containingObject, double speedRatio )
{
SetSpeedRatioImpl(containingObject, speedRatio);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, set the speed ratio on the clock with the
/// given parameters.
/// </summary>
public void SetSpeedRatio( double speedRatio )
{
SetSpeedRatioImpl(this, speedRatio);
}
private void SetSpeedRatioImpl( DependencyObject containingObject, double speedRatio )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.SetSpeedRatio);
if (clock != null)
{
clock.Controller.SpeedRatio = speedRatio;
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call skip-to-fill on the clock.
/// </summary>
public void SkipToFill( FrameworkElement containingObject )
{
SkipToFillImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call skip-to-fill on the clock.
/// </summary>
public void SkipToFill( FrameworkContentElement containingObject )
{
SkipToFillImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call skip-to-fill on the clock.
/// </summary>
public void SkipToFill()
{
SkipToFillImpl(this);
}
private void SkipToFillImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.SkipToFill);
if (clock != null)
{
clock.Controller.SkipToFill();
}
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call stop on the clock.
/// </summary>
public void Stop( FrameworkElement containingObject )
{
StopImpl(containingObject);
}
/// <summary>
/// Given an object, look on the clock store for a clock that was
/// generated from 'this' storyboard. If found, call stop on the clock.
/// </summary>
public void Stop( FrameworkContentElement containingObject )
{
StopImpl(containingObject);
}
/// <summary>
/// Look on the clock store for a clock that was generated from this
/// storyboard. If found, call stop on the clock.
/// </summary>
public void Stop()
{
StopImpl(this);
}
private void StopImpl( DependencyObject containingObject )
{
Clock clock = GetStoryboardClock(containingObject, false, InteractiveOperation.Stop);
if (clock != null)
{
clock.Controller.Stop();
}
if( TraceAnimation.IsEnabled )
{
TraceAnimation.TraceActivityItem(
TraceAnimation.StoryboardStop,
this,
Name,
containingObject );
}
}
/// <summary>
/// HybridDictionary for storing the root clock tree for each storyboard.
/// Key: An instance of the Storyboard object.
/// Value: The root of the clock tree that was created from the key object.
/// </summary>
/// <remarks>
/// Another way to describe the key-value relation is that the value
/// clock object's Timeline property points to the Storyboard.
/// </remarks>
private static readonly UncommonField<HybridDictionary> StoryboardClockTreesField = new UncommonField<HybridDictionary>();
/// <summary>
/// Given an object, look in the attached storage for storyboard clocks
/// and retrieve the one that is associated with 'this' Storyboard instance.
/// throw if not found.
/// </summary>
private Clock GetStoryboardClock(DependencyObject o)
{
return GetStoryboardClock(o, true, InteractiveOperation.Unknown);
}
/// <summary>
/// Given an object, look in the attached storage for storyboard clocks
/// and retrieve the one that is associated with 'this' Storyboard instance.
/// If the clock is null we'll either throw an exception or emit a trace, depending
/// on the value of the throwIfNull parameter. The InteractiveOperation
/// parameter is used to give more detailed info in the trace.
///
/// </summary>
private Clock GetStoryboardClock(DependencyObject o, bool throwIfNull, InteractiveOperation operation)
{
Clock clock = null;
WeakReference clockReference = null;
HybridDictionary clocks = StoryboardClockTreesField.GetValue(o);
if (clocks != null)
{
clockReference = clocks[this] as WeakReference;
}
if (clockReference == null)
{
if (throwIfNull)
{
// This exception indicates that the storyboard has never been applied.
// We check the weak reference because the only way it can be null
// is if it had never been put in the dictionary.
throw new InvalidOperationException(SR.Storyboard_NeverApplied);
}
else if (TraceAnimation.IsEnabledOverride )
{
TraceAnimation.Trace(
TraceEventType.Warning,
TraceAnimation.StoryboardNotApplied,
operation,
this,
o);
}
}
if (clockReference != null)
{
clock = clockReference.Target as Clock;
// At this point the clock may have been garbage collected.
// We may have a null clock even though this Storyboard had
// been applied to the given DependencyObject. One way this
// can happen is if another Storyboard Begins an animation
// on that same DO / DP pair with SnapshotAndReplace semantics.
// In that case AnimationStorage will toss out the old clock.
}
return clock;
}
/// <summary>
/// Given an object, and a clock to associate with 'this' Storyboard
/// instance, save a reference to the clock on the object's attached storage
/// for storyboard clocks. We are storing a weak reference so that the
/// clock is not kept alive. Currently we don't have a way of removing
/// clocks from the list when it is no longer required.
/// </summary>
/// <remarks>
/// We don't care if there's already a clock there - if there is one,
/// the reference is overridden in the HybridDictionary, and the old clock
/// is abandoned.
/// </remarks>
private void SetStoryboardClock(DependencyObject o, Clock clock)
{
HybridDictionary clocks = StoryboardClockTreesField.GetValue(o);
if (clocks == null)
{
clocks = new HybridDictionary();
StoryboardClockTreesField.SetValue(o, clocks);
}
clocks[this] = new WeakReference(clock);
return;
}
/// <summary>
/// The complex path clone storage field stores the clone that we're using
/// in place of the original value.
/// </summary>
/// <remarks>
/// This field is attached to the target object from which the path
/// starts. The field is a map indexed by the property affected. For the
/// example
///
/// TargetName="Rect1" TargetProperty="(Rectangle.LayoutTransform).(RotateTransform.Angle)"
///
/// The FrugalMap will be attached to whatever "Rect1" is. The data
/// will then be stored in the FrugalMap at the index for the property
/// (in this case the LayoutTransformProperty.GlobalIndex)
/// </remarks>
private static readonly UncommonField<FrugalMap> ComplexPathCloneField = new UncommonField<FrugalMap>();
private static CloneCacheEntry GetComplexPathClone(DependencyObject o, DependencyProperty dp)
{
FrugalMap clonesMap = ComplexPathCloneField.GetValue(o);
// FrugalMap is a struct, so no need to check against null.
// when there is no clones field on this object we will get a FrugalMap with no elements.
object value = clonesMap[dp.GlobalIndex];
if (value != DependencyProperty.UnsetValue)
{
return (CloneCacheEntry)clonesMap[dp.GlobalIndex];
}
else
{
return null;
}
}
private static void SetComplexPathClone(
DependencyObject o,
DependencyProperty dp,
object source,
object clone)
{
FrugalMap clonesMap = ComplexPathCloneField.GetValue(o);
if (clone != DependencyProperty.UnsetValue)
{
clonesMap[dp.GlobalIndex] = new CloneCacheEntry(source, clone);
}
else
{
clonesMap[dp.GlobalIndex] = DependencyProperty.UnsetValue;
}
// FrugalMap is a struct - after a change it needs to be set back on the object.
ComplexPathCloneField.SetValue(o, clonesMap);
}
// This is the entry in the ComplexPathClone cache
private class CloneCacheEntry
{
internal CloneCacheEntry(object source, object clone)
{
Source = source;
Clone = clone;
}
internal object Source;
internal object Clone;
}
// Small object used to send a property invalidation when the InvalidatePropertyOnChange
// delegate is called in response to an event.
// The ChangeListener class supports Storyboard animation scenarios with
// multi-step property paths. In these cases, a clone of the original
// value is made and the storyboard animation is attached to the clone.
// This class listens to the changes on both the original object and the
// clone.
// If the original object has changed, this class signals the need to
// re-clone in order to pick up the state of the original object.
// If the cloned object has changed, this class signals an animation-
// driven sub-property invalidation.
internal class ChangeListener
{
// Constructor of the object, the parameters include the property to
// invalidate and the object to invalidate it on. As well as the
// two Freezable objects (original and clone) that are associated
// with the property on the target object.
internal ChangeListener( DependencyObject target, Freezable clone, DependencyProperty property, Freezable original )
{
Debug.Assert( target != null && clone != null && property != null && original != null,
"Internal utility class requires non-null arguments. Check the caller of this method for an error.");
_target = target;
_property = property;
_clone = clone;
_original = original;
}
// Called when the clone has changed. We check the clone cache on
// the target object to see if we were the most recent clone. If so,
// signal a sub-property invalidation. If not, we are no longer
// relevant and we should clean up.
internal void InvalidatePropertyOnCloneChange( object source, EventArgs e )
{
CloneCacheEntry cacheEntry = GetComplexPathClone( _target, _property );
// If the changed freezable is the currently outstanding instance
// then we need to trigger a sub-property invalidation.
if( cacheEntry != null && cacheEntry.Clone == _clone )
{
_target.InvalidateSubProperty(_property);
}
// Otherwise, we are no longer relevant and need to clean up.
else
{
Cleanup();
}
}
// This is the event handler on the original. When the original
// changes, the clone is no longer valid. This method triggers a
// re-clone by calling InvalidateProperty, then clean up. Now that
// the associated clone is no longer valid, there's nothing useful
// for us to listen on.
internal void InvalidatePropertyOnOriginalChange( object source, EventArgs e )
{
// recompute animated value
_target.InvalidateProperty(_property);
Cleanup();
}
// This is the internal method called to set up the listeners on both
// the original and the clone.
internal static void ListenToChangesOnFreezable(
DependencyObject target,
Freezable clone,
DependencyProperty dp,
Freezable original)
{
ChangeListener listener = new ChangeListener( target, clone, dp, original );
listener.Setup();
}
private void Setup()
{
EventHandler changeEventHandler = new EventHandler(InvalidatePropertyOnCloneChange);
// Listen to changes on clone.
_clone.Changed += changeEventHandler;
if( _original.IsFrozen )
{
// We skip setting up for the original object when it is Frozen,
// because it won't change so we don't need to worry about listening.
_original = null;
}
else
{
// If the original is not Frozen, we do need to listen and
// signal a re-clone if the original changes.
changeEventHandler = new EventHandler(InvalidatePropertyOnOriginalChange);
_original.Changed += changeEventHandler;
}
}
// Stop listening to the Changed event on the given Freezable objects
// and clean up.
private void Cleanup()
{
// Remove ourself from the clone
EventHandler changeEventHandler = new EventHandler(InvalidatePropertyOnCloneChange);
_clone.Changed -= changeEventHandler;
// If we're listening on the original, remove ourselves from there too.
// (In Setup() _original was nulled out if we aren't listening.)
if( _original != null )
{
changeEventHandler = new EventHandler(InvalidatePropertyOnOriginalChange);
_original.Changed -= changeEventHandler;
}
// Clear all object references.
_target = null;
_property = null;
_clone = null;
_original = null;
}
DependencyObject _target; // The object to invalidate
DependencyProperty _property; // The property to invalidate on the above object.
Freezable _clone; // The cloned Freezable whose Changed event we were listening to.
Freezable _original; // The original Freezable whose Changed event we're also listening to.
}
internal static class Layers
{
internal static Int64 ElementEventTrigger = 1;
internal static Int64 StyleOrTemplateEventTrigger = 1;
internal static Int64 Code = 1;
internal static Int64 PropertyTriggerStartLayer = 2; // First PropertyTrigger takes this layer number.
}
// Describes the various interactive operations we can do to a controllable
// storyboard. Used by GetStoryboardClock for debug tracing.
private enum InteractiveOperation : ushort
{
Unknown = 0,
Pause,
Remove,
Resume,
Seek,
SeekAlignedToLastTick,
SetSpeedRatio,
SkipToFill,
Stop
}
}
}
|