define animations and triggers as reusable resource?
Asked Answered
H

4

15

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?

Herwin answered 31/10, 2009 at 13:16 Comment(0)
H
1

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.

Herwin answered 10/11, 2009 at 10:42 Comment(6)
Ya know I tried a bunch of different things to come up with a suggestion for this and it just wasn't possible without doing something in code. This is as good a solution as I think you're going to get. Hurray for WPF extensibility, right? :)Semivitreous
It might be a good idea to use the silverlight behavior API for this. Do you agree? IS it compatible with the standard .net 3.5 framework?Herwin
No it's not compatible, though they're supposed. Honestly I think you've found a great solution that leverages the WPF architecture in a very "natural" way and shouldn't second guess yourself at all here.Semivitreous
Sorry, it's late and I realized kinda mispoke there. First off, it's not that they're not compatible, it's just that they're not natively in .NET 3.5. They come with Blend right now via System.Windows.Interactivity. What you've done here is implement what has been referred to as the "Attached Behavior" pattern of WPF and that's how we accomplished the kinda stuff behaviors provide before System.Windows.Interactivity. :)Semivitreous
I don't really like my approach due to the lack of designability. What if a designer would like to completely redesign the animations? I gues the best I can do is parametries the attached property more and the designer would have to handedit the xaml. Do you agree?Herwin
Off-topic, but I'm seeing something very weird here, anybody else seeing this: i.imgur.com/gWK9o.png ???Rondo
M
4

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.
Merwin answered 6/11, 2009 at 22:38 Comment(0)
H
1

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.

Herwin answered 10/11, 2009 at 10:42 Comment(6)
Ya know I tried a bunch of different things to come up with a suggestion for this and it just wasn't possible without doing something in code. This is as good a solution as I think you're going to get. Hurray for WPF extensibility, right? :)Semivitreous
It might be a good idea to use the silverlight behavior API for this. Do you agree? IS it compatible with the standard .net 3.5 framework?Herwin
No it's not compatible, though they're supposed. Honestly I think you've found a great solution that leverages the WPF architecture in a very "natural" way and shouldn't second guess yourself at all here.Semivitreous
Sorry, it's late and I realized kinda mispoke there. First off, it's not that they're not compatible, it's just that they're not natively in .NET 3.5. They come with Blend right now via System.Windows.Interactivity. What you've done here is implement what has been referred to as the "Attached Behavior" pattern of WPF and that's how we accomplished the kinda stuff behaviors provide before System.Windows.Interactivity. :)Semivitreous
I don't really like my approach due to the lack of designability. What if a designer would like to completely redesign the animations? I gues the best I can do is parametries the attached property more and the designer would have to handedit the xaml. Do you agree?Herwin
Off-topic, but I'm seeing something very weird here, anybody else seeing this: i.imgur.com/gWK9o.png ???Rondo
H
1

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.

Huppah answered 26/1, 2018 at 23:12 Comment(7)
You should test for default before setting each part of the animation, otherwise you end up overriding cases where you have an animation where, for instance, the TargetName of a cloned animation is dynamically changed but the TargetProperty remains the same.Jubilant
Beyond the above, I don't understand why this question+answer has received so little response. This seems like a very idiomatic answer that's completely requisite to animation reuse in XAML. So I echo the sentiment of the original question. Are we missing something straight forward? Because, if not, how is everybody else dealing with XAML + reuse?Jubilant
Also one other small issue: the conditional should test for !IsShared.Jubilant
Regarding the condition, I think it's correct right now. By default, if you pull a resource from ResourceDictionary, it creates a fresh instance each time. However, if you mark the resource with x:Shared="True" attribute, the dictionary will only create one instance and return it each time the resource is requested. Now we want to make sure that we're working with a fresh animation every time, so if we detect that the resource is shared (hence current condition), we clone it. Regarding the first comment, I'm not sure I understand what you mean. Can you please clarify?Huppah
What I meant on testing for defaults is imagine you have an animation that, itself, targets some property - let's say opacity. You then use this extension to set the target of the resource with some syntax along the lines of: <utils:AnimationResource ResourceKey="MyAnimation" TargetName="SomeElement" /> you would end up overwriting the target property and it would go from Opacity to null. So it makes sense for the extension to first test for whether or not a value has been provided, before nulling out that value.Jubilant
By default static resources are shared, but that's not so important. I now understand your logic on setting the conditional test to true, but it leads (in my opinion) to confusing semantics. If I specify IsShared=true when declaring the resource using the extension, I would not expect the extension to clone the resource. But this is of course just going to be a matter of semantic preference, one way or the other.Jubilant
Ok, I see your point, and I agree that from that perspective it is somewhat misleading. Perhaps IsShared could be renamed to something else, more indicative of its purpose. Also, I understand now what you meant with checking for defaults. I think it would be a nice feature to have. I'm not sure however if it should be included in the answer, because it would require adding some boilerplate code, which could obscure the idea behind the solution. I intentionally tried to keep it as simple as possible. But feel free to edit the answer if you disagree :)Huppah
I
0

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.

Inheritor answered 18/12, 2009 at 19:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.