9

Consider this code:

<UserControl x:Class="MyApp.MyControl"
             ...
         xmlns:local="clr-namespace:MyApp"
         DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">

    <UserControl.Template>
        <ControlTemplate>
            <ControlTemplate.Resources>
                <Storyboard x:Key="MyStory">
                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
                        <SplineColorKeyFrame KeyTime="0:0:1" Value="Red"/>
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </ControlTemplate.Resources>

            <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
                ...
            </Border>

            <ControlTemplate.Triggers>
                <Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
                    <Trigger.EnterActions>
                        <BeginStoryboard Storyboard="{StaticResource MyStory}"/>
                    </Trigger.EnterActions>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </UserControl.Template>
</UserControl>

The above code works with no problem. Now, I wanna bind key-frame value of MyStory to a DP (named SpecialColor) of this user-control like so:

<Storyboard x:Key="MyStory">
    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
    </ColorAnimationUsingKeyFrames>
</Storyboard>

which makes an error:

Cannot freeze this Storyboard timeline tree for use across threads.

It's possible to do this using code behind. But how can I do it in XAML only?


Code-Behind Aided Solution:

Step 1: Putting the MyStory storyboard into the brdBase resources.

<UserControl.Template>
    <ControlTemplate>
        <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
            <Border.Resources>
                <Storyboard x:Key="MyStory">
                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
                        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </Border.Resources>
            ...
        </Border>

        <ControlTemplate.Triggers>
            <Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource MyStory}"/>
                </Trigger.EnterActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</UserControl.Template>

Error: Cannot find resource named 'MyStory'. Resource names are case sensitive.

Step 2: Eliminating Trigger on IsMouseOver property and begin the MyStory from code behind.

<UserControl.Template>
    <ControlTemplate>
        <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black" MouseEnter="brdBase_MouseEnter">
            <Border.Resources>
                <Storyboard x:Key="MyStory">
                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
                        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </Border.Resources>
        </Border>
    </ControlTemplate>
</UserControl.Template>

C# Code-Behind:

private void brdBase_MouseEnter(object sender, MouseEventArgs e)
{
   Border grdRoot = (Border)this.Template.FindName("brdBase", this);
   Storyboard story = grdRoot.Resources["MyStory"] as Storyboard;

   story.Begin(this, this.Template);
}

Step 3: The solution is already done, but it doesn't work at the first time. Fortunately, there is a workaround for this issue. It's enough to put the ControlTemplate in a Style.

(I need other Trigger types than EventTrigger and must wrap the UserControl elements with the ControlTemplate.)


Update:

The idea about using ObjectDataProvider failed.

  1. An ObjectDataProvider resource cannot be used to provide a storyboard!!! The error report is:
    • XamlParseException: Set property 'System.Windows.Media.Animation.BeginStoryboard.Storyboard' threw an exception.
    • InnerException: 'System.Windows.Data.ObjectDataProvider' is not a valid value for property 'Storyboard'.
  2. The AssociatedControl DP is always null.

Here is the code:

<UserControl.Template>
    <ControlTemplate>
        <ControlTemplate.Resources>
            <local:StoryboardFinder x:Key="StoryboardFinder1" AssociatedControl="{Binding ElementName=brdBase}"/>
            <ObjectDataProvider x:Key="dataProvider" ObjectInstance="{StaticResource StoryboardFinder1}" MethodName="Finder">
                <ObjectDataProvider.MethodParameters>
                    <sys:String>MyStory</sys:String>
                </ObjectDataProvider.MethodParameters>
            </ObjectDataProvider>
        </ControlTemplate.Resources>

        <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
            <Border.Resources>
                <Storyboard x:Key="MyStory">
                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
                        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:MyControl}}, Path=SpecialColor}"/>
                    </ColorAnimationUsingKeyFrames>
                </Storyboard>
            </Border.Resources>
            ...
        </Border>

        <ControlTemplate.Triggers>
            <Trigger SourceName="brdBase" Property="IsMouseOver" Value="True">
                <Trigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource dataProvider}"/>
                </Trigger.EnterActions>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
</UserControl.Template>

The StoryboardFinder class:

public class StoryboardFinder : DependencyObject
{
    #region ________________________________________  AssociatedControl

    public Control AssociatedControl
    {
        get { return (Control)GetValue(AssociatedControlProperty); }
        set { SetValue(AssociatedControlProperty, value); }
    }

    public static readonly DependencyProperty AssociatedControlProperty =
        DependencyProperty.Register("AssociatedControl",
                                    typeof(Control),
                                    typeof(StoryboardFinder),
                                    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None));

    #endregion

    public Storyboard Finder(string resourceName)
    {
        //
        // Associated control is always null :(
        //
        return new Storyboard();
    }
}
Mehdi
  • 2,194
  • 2
  • 25
  • 39

2 Answers2

4

What if this code was true?

<UserControl x:Class="MyApp.MyControl"
             ...
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:l="clr-namespace:MyApp"
             DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">

    <UserControl.Resources>
        <Style TargetType="{x:Type l:MyControl}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type l:MyControl}">
                        <Border x:Name="brdBase" BorderThickness="1" BorderBrush="Cyan" Background="Black">
                            <Border.Resources>
                                <Storyboard x:Key="MyStory">
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
                                        <SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type l:MyControl}}, Path=SpecialColor}"/>
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </Border.Resources>

                            <i:Interaction.Triggers>
                                <l:InteractiveTrigger Property="IsMouseOver" Value="True">
                                    <l:InteractiveTrigger.CommonActions>
                                        <BeginStoryboard Storyboard="{StaticResource MyStory}"/>
                                    </l:InteractiveTrigger.CommonActions>
                                </l:InteractiveTrigger>
                            </i:Interaction.Triggers>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
</UserControl>

If so, I could have a Trigger on IsMouseOver property...

I'm glad to say it's a working code :) I could only use EventTrigger in <Border.Triggers> tag. It was the limitation. So I started thinking about this idea: What if I could have a custom trigger which can work in FrameworkElement.Triggers scope? Here is the code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Interactivity;
using System.Windows.Media.Animation;

namespace TriggerTest
{
    /// <summary>
    /// InteractiveTrigger is a trigger that can be used as the System.Windows.Trigger but in the System.Windows.Interactivity.
    /// <para>
    /// Note: There is neither `EnterActions` nor `ExitActions` in this class. The `CommonActions` can be used instead of `EnterActions`.
    /// Also, the `Actions` property which is of type System.Windows.Interactivity.TriggerAction can be used.
    /// </para>
    /// <para> </para>
    /// <para>
    /// There is only one kind of triggers (i.e. EventTrigger) in the System.Windows.Interactivity. So you can use the following triggers in this namespace:
    /// <para>1- InteractiveTrigger : Trigger</para>
    /// <para>2- InteractiveMultiTrigger : MultiTrigger</para>
    /// <para>3- InteractiveDataTrigger : DataTrigger</para>
    /// <para>4- InteractiveMultiDataTrigger : MultiDataTrigger</para>
    /// </para>
    /// </summary>
    public class InteractiveTrigger : TriggerBase<FrameworkElement>
    {
        #region ___________________________________________________________________________________  Properties

        #region ________________________________________  Value

        /// <summary>
        /// [Wrapper property for ValueProperty]
        /// <para>
        /// Gets or sets the value to be compared with the property value of the element. The comparison is a reference equality check.
        /// </para>
        /// </summary>
        public object Value
        {
            get { return (object)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public static readonly DependencyProperty ValueProperty =
            DependencyProperty.Register("Value",
                                        typeof(object),
                                        typeof(InteractiveTrigger),
                                        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.None, OnValuePropertyChanged));

        private static void OnValuePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            InteractiveTrigger instance = sender as InteractiveTrigger;

            if (instance != null)
            {
                if (instance.CanFire)
                    instance.Fire();
            }
        }

        #endregion


        /// <summary>
        /// Gets or sets the name of the object with the property that causes the associated setters to be applied.
        /// </summary>
        public string SourceName
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the property that returns the value that is compared with this trigger.Value property. The comparison is a reference equality check.
        /// </summary>
        public DependencyProperty Property
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a collection of System.Windows.Setter objects, which describe the property values to apply when the trigger object becomes active.
        /// </summary>
        public List<Setter> Setters
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the collection of System.Windows.TriggerAction objects to apply when this trigger object becomes active.
        /// </summary>
        public List<System.Windows.TriggerAction> CommonActions
        {
            get;
            set;
        }

        /// <summary>
        /// Gets a value indicating whether this trigger can be active to apply setters and actions.
        /// </summary>
        private bool CanFire
        {
            get
            {
                if (this.AssociatedObject == null)
                {
                    return false;
                }
                else
                {
                    object associatedValue;

                    if (string.IsNullOrEmpty(SourceName))
                        associatedValue = this.AssociatedObject.GetValue(Property);
                    else
                        associatedValue = (this.AssociatedObject.FindName(SourceName) as DependencyObject).GetValue(Property);

                    TypeConverter typeConverter = TypeDescriptor.GetConverter(Property.PropertyType);
                    object realValue = typeConverter.ConvertFromString(Value.ToString());

                    return associatedValue.Equals(realValue);
                }
            }
        }

        #endregion


        #region ___________________________________________________________________________________  Methods

        /// <summary>
        /// Fires (activates) current trigger by setting setter values and invoking all actions.
        /// </summary>
        private void Fire()
        {
            //
            // Setting setters values to their associated properties..
            //
            foreach (Setter setter in Setters)
            {
                if (string.IsNullOrEmpty(setter.TargetName))
                    this.AssociatedObject.SetValue(setter.Property, setter.Value);
                else
                    (this.AssociatedObject.FindName(setter.TargetName) as DependencyObject).SetValue(setter.Property, setter.Value);
            }

            //
            // Firing actions.. 
            //
            foreach (System.Windows.TriggerAction action in CommonActions)
            {
                Type actionType = action.GetType();

                if (actionType == typeof(BeginStoryboard))
                {
                    (action as BeginStoryboard).Storyboard.Begin();
                }
                else
                    throw new NotImplementedException();
            }

            this.InvokeActions(null);
        }

        #endregion


        #region ___________________________________________________________________________________  Events

        public InteractiveTrigger()
        {
            Setters = new List<Setter>();
            CommonActions = new List<System.Windows.TriggerAction>();
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            if (Property != null)
            {
                object propertyAssociatedObject;

                if (string.IsNullOrEmpty(SourceName))
                    propertyAssociatedObject = this.AssociatedObject;
                else
                    propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);

                //
                // Adding a property changed listener to the property associated-object..
                //
                DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());
                dpDescriptor.AddValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            if (Property != null)
            {
                object propertyAssociatedObject;

                if (string.IsNullOrEmpty(SourceName))
                    propertyAssociatedObject = this.AssociatedObject;
                else
                    propertyAssociatedObject = this.AssociatedObject.FindName(SourceName);

                //
                // Removing previously added property changed listener from the associated-object..
                //
                DependencyPropertyDescriptor dpDescriptor = DependencyPropertyDescriptor.FromProperty(Property, propertyAssociatedObject.GetType());
                dpDescriptor.RemoveValueChanged(propertyAssociatedObject, PropertyListener_ValueChanged);
            }
        }

        private void PropertyListener_ValueChanged(object sender, EventArgs e)
        {
            if (CanFire)
                Fire();
        }

        #endregion
    }
}

I've also created other trigger types (i.e. InteractiveMultiTrigger, InteractiveDataTrigger, InteractiveMultiDataTrigger) as well as some more actions which makes it possible to have a conditional and multi-conditional EventTriggers. I'll publish them all if you professional guys confirm this solution.

Thanks for your attention!

Mehdi
  • 2,194
  • 2
  • 25
  • 39
  • that's nice, it should work without problems. the only thing I can see is that it might less efficient since you don't freeze your storyboard timeline anymore and it means that it's not using multiple threads. – Erti-Chris Eelmaa Apr 10 '13 at 06:41
  • You are right. I'm looking for a way to invoke the `Invoke` method of `TriggerAction`. Through this way, every required stuff should be done internally. Do have any idea about? (I marked it as `Couldn't use this snippet`.) – Mehdi Apr 10 '13 at 06:53
  • On another view, actually, from docs, Begin() does the freezing automatically. I am more interested about why doesn't Xaml one work correctly? Why are you trying to access internal stuff anyways? It's never good idea, unless absolutely no other way – Erti-Chris Eelmaa Apr 10 '13 at 18:15
  • I tried to freeze the storyboard through `Storyboard.Freeze()` method before beginning it, but it caused this error: _This Freezable cannot be frozen._ Although I can use `Storyboard.GetAsFrozen()` method to create a frozen copy and then begin it. I really don't know if this operation makes the code more efficient. Does it? I read .Net source code of _Storyboard.cs_. If you call `Begin()` method, it'll use `SnapshotAndReplace` as its hand-off-behavior. – Mehdi Apr 11 '13 at 02:08
  • I agree with you, I should not use internal stuff. **Efficiency** is important for me, because I plan to use the InteractiveTrigger (as well as its MultiTrigger, DataTrigger, and MultiDataTrigger versions) widely in my app. – Mehdi Apr 11 '13 at 02:25
  • Marked as answer. However any suggestion about improving its efficiency is appreciated. – Mehdi Apr 12 '13 at 07:50
3

Well, you can't really bind to "To" nor From, because the storyboard has to be frozen, in order to work efficiently with cross-threading.

Solution1) Simplest solution without hacks(involves code-behind): Add MouseOver event handler & in the event handler, locate necessary animation, set the "To" property directly, so you won't use binding and the "freezing" can be done. This way you won't hardcode anything :).

Solution2) There is a cool hack that supports XAML only( a little bit of converter magic ofcourse ), but I do not suggest it. It's cool nonetheless :) WPF animation: binding to the "To" attribute of storyboard animation See answer by Jason.

There are few things more that you can try:

Solution3) Don't use Dependency properties, but rather implement INotifyProperthChanged. This way you still can BIND "To". Note that I think this should theoretically work, but I have not tried.

Solution4) Apply Mode=OneTime to your binding. Maybe it works?

Solution5) Write your own attached behavior that will evaluate dependency property on correct thread and set "To" property. I think that will be nice solution.

Here is good duplicate too: WPF Animation "Cannot freeze this Storyboard timeline tree for use across threads"

Community
  • 1
  • 1
Erti-Chris Eelmaa
  • 25,338
  • 6
  • 61
  • 78
  • Thanks @Erti-Chris Eelmaa. The first solution is not intended. (I gave the complete version above!) Also I don't agree with using the `Tag` property because of its performance limitation. – Mehdi Apr 06 '13 at 16:18
  • I have an idea. ☼ Can we use `ObjectDataProvider` to call a static method that returns any arbitrary resource of an element? – Mehdi Apr 06 '13 at 16:22
  • I added more theoretical solutions. Btw I am not familiar with ObjectDataProvider. I am not sure what you would like to achieve with ObjectDataProvider though. All I see, is that it is a class, that takes a method name, evaluates it, and set's it to collection that is returned so you can bind against it. – Erti-Chris Eelmaa Apr 06 '13 at 17:23
  • We can use `VisualTreeHelper` in that method to find any resource of an element. But we need to pass an element or the user-control as a method parameter first. I couldn't pass it. (I'm still working on it.) – Mehdi Apr 06 '13 at 17:28
  • I tested 3rd and 4th solutions with no success. – Mehdi Apr 06 '13 at 17:37
  • You can derive from ObjectDataProvider and add one property more, then bind it against UI element. Then you have access to it. – Erti-Chris Eelmaa Apr 06 '13 at 18:17
  • Thanks @Erti-Chris Eelmaa. I used the `ObjectInstance` to pass an element. It seems to be correct but doesn't work. I added an **Update** to the question. – Mehdi Apr 07 '13 at 03:57
  • May you describe more about **5th solution**? Can you give some code or tell me what should I do exactly? – Mehdi Apr 07 '13 at 09:04
  • Mimi what have you tried and what error you had with 5th solution? I can't provide full-implementation since I don't have that much free time. Have a new attached property BindableValue, that you can set, what will control "Value" property – Erti-Chris Eelmaa Apr 08 '13 at 14:42
  • +1 Thanks for your attention. I found a solution and currently trying to complete and test it. I used `System.Windows.Interactivity` to create a new `Trigger` in order to use it in scopes that usual Triggers aren't available. (i.e. everywhere except `Template` and `Style`) Through this way, I can achieve my primary goal in using dynamic storyboards. I've succeeded to create `InteractiveTrigger`, `ConditionalEventTrigger` and `MultiConditionalEventTrigger` up to now. They've passed initial tests. I'm trying to create `InteractiveDataTrigger` and `InteractiveMultiDataTrigger` now. – Mehdi Apr 08 '13 at 15:42
  • `InteractiveTrigger` (as I named it) is a trigger that can be used as the `System.Windows.Trigger` but in the `System.Windows.Interactivity`. There is only one kind of triggers in the `System.Windows.Interactivity` which is `EventTrigger`. – Mehdi Apr 08 '13 at 15:50