C# – How to access a storyboard within an element resources from XAML

bindingcstoryboardwpfxaml

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();
    }
}

Best Answer

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"

Related Topic