I’ve just had the displeasure of burning quite a few hours on a stupid problem with starting animations from a control template using data binding and datatriggers in WPF. I’ve solved it eventually but only after wasting way too long on something I think should have been pretty straight forward.
Anyway, here’s the problem definition.
Write a control to display the state of a variable representing the status of a request to a server. Pretty simple? Well, you would have thought so. But as usual, the ‘lets make it cool’ part of my programmer head got in the way, an I found myself wanting to animate the control.
Ok, nothing amazing, just make it fade up nicely when it changes state, blink while it’s the request is in progress, and fade out when were done. Not to much to ask for!
So, I create a sample application and added the server graphics
This icon represents when the request is downloading information and fades up first, then blinks when at maximum opacity. (Busy)
This icon is when the request is completed. It fades out after the Busy icon has been flashing.
Lastly, this icon shows when the request failed for whatever reason. Im sure the users aren’t concerned with the inner exception property of the soapexception π
Anyway, wanting to continue with the distinct separation of concerns for the UI & the internal datamodel I have in my application, I created the control’s template to
respond to changes in the data, rather than have the code drive the visual changes in the UI. By assuming a Pull model for the UI, we can completely decouple the interface from the application logic.
So, I created a tiny test datamodel that we will use for the DataBinding of the control.
1: public enum eServerState
2: {
3: Idle,
4: Busy,
5: Complete,
6: Failed
7: };
8:
9: public class MyData : INotifyPropertyChanged
10: {
11:
12: public event PropertyChangedEventHandler PropertyChanged;
13:
14: private eServerState serverState = eServerState.Idle;
15:
16: public eServerState ServerState
17: {
18: get { return serverState; }
19: set
20: {
21: serverState = value;
22: OnPropertyChanged("ServerState");
23: }
24: }
25:
26: private void OnPropertyChanged(string s)
27: {
28: if (PropertyChanged != null)
29: PropertyChanged(this, new PropertyChangedEventArgs(s));
30: }
31:
32: }
One thing you might notice is that we’ll be binding to an enum. This means we can avoid having any magic numbers in the Xaml , and we can create combo boxes or list boxes that show the names of the enumeration completely in xaml too.
Next, we have the window that will show a combobox with the enumerations names, and the control itself.
1: <Window.Resources>
2: <ObjectDataProvider MethodName="GetValues"
3: ObjectType="{x:Type System:Enum}"
4: x:Key="ServerStates">
5: <ObjectDataProvider.MethodParameters>
6: <x:Type TypeName="l:eServerState"/>
7: </ObjectDataProvider.MethodParameters>
8: </ObjectDataProvider>
9: </Window.Resources>
10: <Grid>
11: <l:ServerGraphic/>
12: <ComboBox ItemsSource="{Binding Source={StaticResource ServerStates}}" SelectedValue="{Binding Path=ServerState}"
13: IsSynchronizedWithCurrentItem="False" Margin="64,8,64,0" VerticalAlignment="Top" Height="23" HorizontalAlignment="Center" >
14:
15: </ComboBox>
16: </Grid>
You can see that I’ve set up an <ObjectDataProvider> type, that we use to extract the enum’s values. The most excellent Beatriz Costa has plenty of information about Object Data providers and Data sources, if you’re WPF’ing on a regular basis, she should definately be on your regular reading list!
The combo box then binds to that resource and shows us the enumerations names, whilst binding its selected item straight into our datamodel’s ServerState variable. Cool! no event code, no codebehind, and a clean separation all in one step!
Lastly we get to the Control used to represent the server’s status.
Here’s is the Xaml for the control.
1: <UserControl
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: x:Class="DateTemplate1.ServerGraphic"
5: x:Name="UserControl"
6: Width="100" Height="100">
7: <UserControl.Resources>
8: <Storyboard x:Key="FadeUpAndFlash">
9: <DoubleAnimation From="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="0:0:10" FillBehavior="Stop" />
10: </Storyboard>
11: <ControlTemplate x:Key="serverTemplate" >
12: <Image x:Name="theImage"/>
13:
14: <ControlTemplate.Triggers>
15:
16: <DataTrigger Binding="{Binding Path=ServerState}" Value="Busy" >
17: <DataTrigger.Setters>
18: <Setter TargetName="theImage" Property="Source" Value="download_server.png"></Setter>
19: </DataTrigger.Setters>
20: <DataTrigger.EnterActions>
21: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="busyAnim" />
22: </DataTrigger.EnterActions>
23: <DataTrigger.ExitActions>
24: <StopStoryboard BeginStoryboardName="busyAnim" />
25: </DataTrigger.ExitActions>
26: </DataTrigger>
27:
28: <DataTrigger Binding="{Binding Path=ServerState}" Value="Complete">
29: <DataTrigger.Setters>
30: <Setter TargetName="theImage" Property="Source" Value="enable_server.png"></Setter>
31: </DataTrigger.Setters>
32: <DataTrigger.EnterActions>
33: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="completeAnim" />
34: </DataTrigger.EnterActions>
35: <DataTrigger.ExitActions>
36: <StopStoryboard BeginStoryboardName="completeAnim" />
37: </DataTrigger.ExitActions>
38: </DataTrigger>
39:
40: <DataTrigger Binding="{Binding Path=ServerState}" Value="Failed">
41: <DataTrigger.Setters>
42: <Setter TargetName="theImage" Property="Source" Value="desable_server.png"></Setter>
43: </DataTrigger.Setters>
44: <DataTrigger.EnterActions>
45: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="failedAnim" />
46: </DataTrigger.EnterActions>
47: <DataTrigger.ExitActions>
48: <StopStoryboard BeginStoryboardName="failedAnim" />
49: </DataTrigger.ExitActions>
50: </DataTrigger>
51:
52: </ControlTemplate.Triggers>
53: </ControlTemplate>
54: </UserControl.Resources>
55: <Grid x:Name="LayoutRoot">
56: <ContentControl Template="{StaticResource serverTemplate}" />
57: </Grid>
58: </UserControl>
Animation Problems…
On running the application I notice that only one of the application consistently starts its icon fading up. This also happens to be the last trigger in the datatriggers. When I change states, the other icons just appear at whatever state the animation is currently at. Wierd.
I check my code, and although I started with <StopStoryboard> in the exit actions, i replace that with <RemoveStoryboard> and re-run.
Nothing. No change. The icons still fade up without restarting their storboards.
So the search begins. I fiddle withe various other settings on the storyboard, and anything that i think may affect the transitions from one storyboard to the next.
The way I originally see the code working is that when a Storyboard is started in the <DataTrigger.EnterActions> I should undo that in the <DataTrigger.ExitActions>, which i assume gets called when the Data that is causing this trigger to fire changes state AWAY from this triggers value. Seems sensible to me anyway.
So off I go, and even google isn’t my friend here turning up nothing after almost 4 hours of trawling various groups blogs and mailing lists. Even Beatriz has let me down!
Next I fire off a mail to a windows mailing list I’m on, and someone mails me back with a link to an MSDN Forum on windows 2008 & WPF!. I’d missed this even though my initial searches turned up old Avalon forums on MSDN.
Anyway, after reading this MSDN forum post which has exactly the solution to my problem presented by none other than the good doctor himself, Dr. WPF.
In response to the question Dr WPF recommends stopping any animation that may be playing before starting this new states animation. There is no mention of using <DataTrigger.ExitActions> to do this, even though it seems sensible that this would be just the place to do that.
So, after taking Dr WPF’s advice, I modify my trigger’s to the code below.
1: <UserControl
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: x:Class="DateTemplate1.ServerGraphic"
5: x:Name="UserControl"
6: Width="100" Height="100">
7: <UserControl.Resources>
8: <Storyboard x:Key="FadeUpAndFlash">
9: <DoubleAnimation From="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)" Duration="0:0:10" FillBehavior="Stop" />
10: </Storyboard>
11: <ControlTemplate x:Key="serverTemplate" >
12: <Image x:Name="theImage"/>
13:
14: <ControlTemplate.Triggers>
15:
16: <DataTrigger Binding="{Binding Path=ServerState}" Value="Busy" >
17: <DataTrigger.Setters>
18: <Setter TargetName="theImage" Property="Source" Value="download_server.png"></Setter>
19: </DataTrigger.Setters>
20: <DataTrigger.EnterActions>
21: <StopStoryboard BeginStoryboardName="failedAnim" />
22: <StopStoryboard BeginStoryboardName="completeAnim" />
23: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="busyAnim" />
24: </DataTrigger.EnterActions>
25: </DataTrigger>
26:
27: <DataTrigger Binding="{Binding Path=ServerState}" Value="Complete">
28: <DataTrigger.Setters>
29: <Setter TargetName="theImage" Property="Source" Value="enable_server.png"></Setter>
30: </DataTrigger.Setters>
31: <DataTrigger.EnterActions>
32: <StopStoryboard BeginStoryboardName="busyAnim" />
33: <StopStoryboard BeginStoryboardName="failedAnim" />
34: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="completeAnim" />
35: </DataTrigger.EnterActions>
36: </DataTrigger>
37:
38: <DataTrigger Binding="{Binding Path=ServerState}" Value="Failed">
39: <DataTrigger.Setters>
40: <Setter TargetName="theImage" Property="Source" Value="desable_server.png"></Setter>
41: </DataTrigger.Setters>
42: <DataTrigger.EnterActions>
43: <StopStoryboard BeginStoryboardName="busyAnim" />
44: <StopStoryboard BeginStoryboardName="completeAnim" />
45: <BeginStoryboard Storyboard="{StaticResource FadeUpAndFlash}" Name="failedAnim" />
46: </DataTrigger.EnterActions>
47: </DataTrigger>
48:
49: </ControlTemplate.Triggers>
50: </ControlTemplate>
51: </UserControl.Resources>
52: <Grid x:Name="LayoutRoot">
53: <ContentControl Template="{StaticResource serverTemplate}" />
54: </Grid>
55: </UserControl>
56:
57:
58:
From this new code, you can see that I now stop the other animations that may already be playing before I start the current state’s animation. (eg Lines 21,22)
This fixes the problem, and my server control now correctly reflects the state of the enum in my datamodel!
So, it appears that stopping or removing the storyboards in the ExitActions is a definite no-go.
Thanks Dr WPF!
The example VS2008 solution is available for download below.
Animations with DataTriggers Solution
I’am the problem as you do … Tried out your code but when I stop the storyboards let say I wanted too animate a margin .. from etc. 3 different defined Enum states .. When I use the stopStoryboard the animation resets, so the margin animation is not fluid between the states .. Do you have any idea how to make the elements keep their properties when a storyboard gets the stopStoryboard event. ?.
Hi Bobo. I’m not really I understand what your problem is. It sounds like the changing of your animation causes jumps?
One way of ensuring smooth transitions is to omit the starting points of the transition (get rid of the “From” in the xaml) this way, all the animations just go from their current position towards their next position.
is that what you mean?
Cheers!
I am having same problem, just backwards, i just WANT the storyboard to START on my image! π I want it to start when in the code behind listed below imageTagSet = true;
Can you tell me what’s wrong with this? PLEASE?!? π
CODE BEHIND
__________________________
public partial class Window1 : Window
{
private bool imageTagSet = false;
public bool ImageTagSet
{
get { return imageTagSet; }
set { imageTagSet = value; }
}
public Window1()
{
InitializeComponent();
imageTagSet = true;
}
}
Hmm… xaml didn’t post.. trying again…
Hi Jenifer. I don’t know without seeing the XAML/full example for context, but one thing i do notice from the snippet you posted is that you aren’t implementing INotifyPropertyChanged and you’re setting the flag in the constructor – possibly not the best place as the visual tree may not be correctly initialised at this point. It’s safer to trigger this sort of thing in the OnLoaded event which guarantees that your window is good to go!
cheers,
Rob