Question

Is there a way to define an animation somewhere in xaml (eg. as a resource) once and then reuse it multiple times? I have a lot of independent brushes across differnt datatemplates that independently need to start the same kind of animation based on a datatrigger. Now since it seems that an animation has to define an Storyboard.TargetName and Storyboard.TargetProperty. This pretty much defeats the purpose of reusability. I would somehow like to declare "use this animation form the resource but apply it to another element this time".

To me this seems to be a rather basic, important and essential request and I am suprised that its not that straight forward to acomplish. Am I missing something here?

The same thing applies for triggers. Suppose I have a lot of differnt visual elements that all represent the same type of state using color animations. E.g. fade to green when "active" fade to "red" when "error" etc. The only difference between the visuals is their shape/visual tree the desired animation behavior is the same, they all have an element somewhere in their visual tree that has a property of type color. I think it is not hard to imagine how tedious it is to redefine the same animations and datatrigger sets over and over again. Every developer hates this. I desperately seek for an easier solution that doesn't require no (or at least very little) c# code behind.

What I have come up with so far is this:

Define the animations in a resource lik this (repeat this for all basic states that there are, like activating, active, inactive, error):

<ColorAnimationUsingKeyFrames x:Key="deactivatingColorAnimation" 
                    Storyboard.TargetProperty="Material.(MaterialGroup.Children)[0].Brush.(SolidColorBrush.Color)"                    
                    FillBehavior="HoldEnd" RepeatBehavior="Forever" AutoReverse="True">
      <ColorAnimationUsingKeyFrames.KeyFrames>
        <LinearColorKeyFrame KeyTime="00:00:00" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.25" Value="Gray"/>
        <LinearColorKeyFrame KeyTime="00:00:0.5" Value="Gray" />
        <LinearColorKeyFrame KeyTime="00:00:0.75" Value="Gray" />
     </ColorAnimationUsingKeyFrames.KeyFrames>
</ColorAnimationUsingKeyFrames>

The use it in storyboard in the triggers (repeat this zillions of times for each state X each differnt stateviusal, always come up with a new name for the storyboard):

<DataTrigger Binding="{Binding SubstrateHolder.State}" Value="Deactivating">
        <DataTrigger.EnterActions>
            <BeginStoryboard x:Name="someStateVisualDeactivatingStoryboard">
                <Storyboard Storyboard.TargetName="someStateVisual">
                    <StaticResource ResourceKey="deactivatingColorAnimation" />
                </Storyboard>
            </BeginStoryboard>
        </DataTrigger.EnterActions>
        <DataTrigger.ExitActions>
            <RemoveStoryboard BeginStoryboardName="someStateVisualDeactivatingStoryboard" />
        </DataTrigger.ExitActions>
</DataTrigger>

You can easily imagine how much bloat XAML I have to repeatedly copy and paste for all those zillion DataTriggers.

It would be cool to define all this triggers once and apply it to different state visuals. How is something like this solved in WPF? Any tip?

Was it helpful?

Solution 2

There doesn't seem to be any good XAML-only solution to this general problem. I ended up writing my own attached properties that define all the animation behaviors for a given element. Something like this:

<DataTemplate>
   <!-- ...  -->
   <Rectangle Fill="Gray">
     <v:AnimationHelper.Animations>
        <v:StandardColorStateAnimation TargetColorProperty="(Rectangle.Fill).(SolidColorBrush.Color)" TargetStateProperty={Binding State} />
     </v:AnimationHelper.Animations>
   </Rectangle>
<DataTemplate>

The rest (creating the animation etc.) is done in the codebehind.

OTHER TIPS

Could you try something like this?

  • Wrap all your current control templates with an invisible root element, e.g. a Border or a StackPanel, whose bounding box will cover the entire control.
  • Create a style or control template for this invisible box that contains all your triggers and animations.
  • Have the animations animate an arbitrary Color property on the invisible box.
  • In the visual trees for all your different controls, bind any properties you want to animate to the Color property on the invisible root element.

I realize this issue is a bit dead at the time of this posting, but I did find a solution that requires very little code behind.

You can make a UserControl with custom properties(Scroll down to about Figure 8) that contains your rectangle as well as the animations and state triggers. This user control would define a public property such as status that would trigger the color change when altered.

The only code-behind required is to create your variables in code.

The user control can them be reused over and over again without rewriting the the XAML storyboards or data triggers.

The most "XAML way" to achieve this goal I can think of is to create dedicated MarkupExtension which would pull the animation from the resources dictionary and set necessary properties - I assume those are limited to a subset of Storyboard.Target, Storyboard.TargetName and Storyboard.TargetProperty. Although it requires some code-behind, it is one-time effort, moreover, MarkupExtensions are designed to be used with XAML. Here's the simplest version:

[MarkupExtensionReturnType(typeof(Timeline))]
public class AnimationResourceExtension : StaticResourceExtension
{
    //This property is for convienience so that we
    //don't have to remember to set x:Shared="False"
    //on all animation resources, but have an option
    //to avoid redundant cloning if it is
    public bool IsShared { get; set; } = true;

    public DependencyObject Target { get; set; }

    public string TargetName { get; set; }

    public PropertyPath TargetProperty { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (base.ProvideValue(serviceProvider) is Timeline animation)
        {
            //If the animation is shared we shall clone it
            //Checking if it is frozen is optional and we can
            //either clone it or throw an exception
            //(or simply proceed knowing one will be thrown anyway)
            if (IsShared || animation.IsFrozen)
                animation = animation.Clone();
            Storyboard.SetTarget(animation, Target);
            Storyboard.SetTargetName(animation, TargetName);
            Storyboard.SetTargetProperty(animation, TargetProperty);
            return animation;
        }
        else
            throw new XamlException("The referenced resource is not an animation");
    }
}

Usage is very straightforward:

<FrameworkElement.Resources>
    <DoubleAnimation x:Key="MyAnimation" From="0" To="1" Duration="0:0:1" />
</FrameworkElement.Resources>
(...)
<Storyboard>
    <utils:AnimationResource ResourceKey="MyAnimation" TargetName="SomeElement" TargetProperty="Opacity" />
</Storyboard>

Being as simple as it can be this solution has its limitations - it does not support neither Binding nor DynamicResource extensions for aforementioned properties. This however is achievable, but requires some extra effort. Binding support should be pretty simple - a question of proper use of XamlSetMarkupExtensionAttribute (plus some boilerplate code). DynamicResource support would be a little trickier, and in addition to using XamlSetMarkupExtensionAttribute would require wrapping the IServiceProvider to return adequate IProvideValueTarget implementation, but is still possible.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top