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> /// 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.Get(SRID.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.Get(SRID.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 ); }
/// <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.Get(SRID.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.Get(SRID.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.Get(SRID.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.Get(SRID.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.Get(SRID.Storyboard_PropertyPathMustPointToDependencyObject, AccessorName(path, i-1), path.Path)); } if( intermediateDP == null ) { throw new InvalidOperationException(SR.Get(SRID.Storyboard_PropertyPathMustPointToDependencyProperty, path.Path )); } if( checkingFrozenState && intermediateDO.IsSealed ) { throw new InvalidOperationException(SR.Get(SRID.Storyboard_PropertyPathSealedCheckFailed, intermediateDP.Name, path.Path, intermediateDO)); } if(!AnimationStorage.IsPropertyAnimatable(intermediateDO, intermediateDP) ) { throw new InvalidOperationException(SR.Get(SRID.Storyboard_PropertyPathIncludesNonAnimatableProperty, path.Path, intermediateDP.Name)); } } } }