Skip Navigation LinksHome > View Post

Animating when data changes

I posted not too long ago about the difficulty of using animation with the Model-View-ViewModel pattern. I recently had a conversation with a customer where they wondered about the best way to create a transition effect when a ViewModel property changed.

After doing some deep thinking about this, it seemed important that the problem needs to be removed from the ViewModel, to keep it canonical and represent only the 'presentation state' (i.e. the current item). Ideally, we could create a generic way of implementing transition on data change and and move the problem from the ViewModel domain, into the View and ideally into a custom Control.

Such a control would have to store two versions of the data (or content) and hold them both long enough to allow the transition to take place. In that case, sub-classing the ContentControl seems like a good place to start, especially as you can handle Content changes by overriding the OnContentChanged method (both in WPF and Silverlight):

public class AnimatedContentControl : ContentControl
{
    public AnimatedContentControl()
    {
        DefaultStyleKey = typeof(AnimatedContentControl);
    }

    protected override void OnContentChanged(object oldContent, object newContent)

We then need a 'container' for both the old content and the new content. I implemented these as ContentControls in a ControlTemplate:

<Style TargetType="local:AnimatedContentControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:AnimatedContentControl">
                <Grid>
                    <ContentControl x:Name="_current" ContentTemplate="{TemplateBinding ContentTemplate}" />
                    <ContentControl x:Name="_old" ContentTemplate="{TemplateBinding ContentTemplate}" />
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>



private ContentControl _old;
private ContentControl _current;

public override void OnApplyTemplate()
{
    _old = (ContentControl)GetTemplateChild("_old");
    _current = (ContentControl)GetTemplateChild("_current");
    base.OnApplyTemplate();
}

Next we need to hook up whenever the content changes and place the old content in the '_old' content control and the new content in the '_new' content control (PS - I realise that I've not used best practice naming conventions here but this is just an experimental spike):

protected override void OnContentChanged(object oldContent, object newContent)
{
    base.OnContentChanged(oldContent, newContent);

    if (_current != null && _old != null)
    {
        _current.Content = newContent;
        _old.Content = oldContent;

        CreateFade(1, 0, _old);
        CreateFade(0, 1, _current);
    }
}

As you can see, I've chosen to use a fade effect in this particular spike and do so programmatically:

private void CreateFade(double from, double to, DependencyObject element)
{
    DoubleAnimation da = new DoubleAnimation();
    da.Duration = new Duration(TimeSpan.FromSeconds(1));
    da.From = from;
    da.To = to;
    Storyboard.SetTarget(da, element);
    Storyboard.SetTargetProperty(da, new PropertyPath("Opacity"));

    Storyboard sb = new Storyboard();
    sb.Children.Add(da);
    sb.Begin();
}

Here's a working example for you to play with (in Silverlight):

The usage of the new control in this sample is pretty simple. We have a ViewModel with Customers property and a SelectedCustomer property and the Xaml in the plug-in looks like this:

<Grid Background="White">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <ListBox ItemsSource="{Binding Customers}"
            SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}"
            DisplayMemberPath="Name" />
    <local:AnimatedContentControl Content="{Binding SelectedCustomer}" Grid.Column="2">
        <local:AnimatedContentControl.ContentTemplate>
            <DataTemplate>
                <Grid Margin="5">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>

                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <TextBlock Text="Name" Grid.Row="0" VerticalAlignment="Center" />
                    <TextBox Text="{Binding Name}" Grid.Column="1" Grid.Row="0" Margin="3"/>

                    <TextBlock Text="Post Code" Grid.Row="1" VerticalAlignment="Center" />
                    <TextBox Text="{Binding PostCode}" Grid.Column="1" Grid.Row="1" Margin="3" />

                </Grid>
            </DataTemplate>
        </local:AnimatedContentControl.ContentTemplate>
    </local:AnimatedContentControl>
</Grid>

Download

You can download the source code right here. BUT, before you download this - read the paragraph below...

What next?

Given this is just an experiment, how might the control be improved? I can think of a bunch of ways: allowing pluggable animations (VSM in Silverlight?), allowing multiple transitions at once (i.e. if the data changes before the animation has finished). Indeed, I started to look into how I might do all this when I came across the TransitionContentControl which is part of the Silverlight Control Toolkit.

This works almost identically to my implementation here but is much more fully implemented. Great, that should save us all some time :)

 
Josh Post By Josh Twist
5:02 AM
06 Apr 2009

» Next Post: Cracking an XPS in WPF
« Previous Post: Microsoft Tag in slightly less cool than I thought Shocker

Comments are closed for this post.

Posted by FallenGameR @ 06 Apr 2009 7:24 AM
There is VSM for WPF: http://windowsclient.net/wpf/wpf35/wpf-35sp1-toolkit-visual-state-manager-overview.aspx

It is released as a library WPFToolkit on CodePlex. But will be officially included in .NET Framework 4.0.

Posted by Carlos @ 08 Apr 2009 3:23 PM
Thank you, Thank you, Thank you. I've been struggling with this scenario for days. I just needed for my view elements to not just disappear when the bound viewModel changed or was set to null.

Posted by Marty @ 09 Apr 2009 12:38 AM
I dont think you actually need the if conditional check in OnContentChanged Method, because either _current or _old contentcontrol should be always non-null, as they are defined in the default style and initialized when the templated parent control is initialized.

Posted by Josh @ 09 Apr 2009 12:43 AM
@Carlos - you're welcome!

@Marty - unfortunately, OnContentChanged can fire before OnApplyTemplates and you get a nasty exception.

Posted by Pradeep Mahdevu @ 14 Apr 2009 5:52 PM
Josh,
Even I faced the same problem for some time, The way I resolved is something similar but not the same... But would like your comments...
I manage my animations in code using this line of code.

<inf:ViewModelChangeDispatcher Source="{Binding XXXXAnimation}" SourceChanged="RunAnimation"/>


I defined ViewModelChangeDispatcher as

####################################


I
public class ViewModelChangeDispatcher : FrameworkElement
{
#region Data

public static readonly DependencyProperty SourceProperty;
public static readonly DependencyProperty OnSourceChangedStartStoryboardProperty;

// The routed event
public static RoutedEvent SourceChangedEvent;

#endregion // Data

#region Static Constructor and static methods


static ViewModelChangeDispatcher()
{
SourceProperty = DependencyProperty.Register(
"Source",
typeof(String),
typeof(ViewModelChangeDispatcher),
new FrameworkPropertyMetadata(string.Empty,OnSourcePropertyChanged));



OnSourceChangedStartStoryboardProperty = DependencyProperty.Register(
"OnSourceChangedStartStoryboard",
typeof(Storyboard),
typeof(ViewModelChangeDispatcher));


//Registering the events
SourceChangedEvent = EventManager.RegisterRoutedEvent("SourceChanged",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ViewModelChangeDispatcher));

}

private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{

ViewModelChangeDispatcher vmcd = d as ViewModelChangeDispatcher;
if (vmcd == null) return;

vmcd.RaiseEvent(new RoutedEventArgs(SourceChangedEvent, vmcd));
if (vmcd.OnSourceChangedStartStoryboard == null) return;
vmcd.OnSourceChangedStartStoryboard.Begin();
}



#endregion

#region constructor and members
public ViewModelChangeDispatcher()
{
this.Visibility = Visibility.Collapsed;
this.Width = 0;
this.Height = 0;


}

/*
* Source is always string.
* It is the onus of the markupextension to convert it to a string.
*
* */
public String Source
{
get { return (String)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}



public event RoutedEventHandler SourceChanged
{
add { AddHandler(SourceChangedEvent, value); }
remove { RemoveHandler(SourceChangedEvent, value); }
}

public Storyboard OnSourceChangedStartStoryboard
{
get { return (Storyboard)GetValue(OnSourceChangedStartStoryboardProperty); }
set { SetValue(OnSourceChangedStartStoryboardProperty, value); }
}
#endregion


}


##############

I could have also written
<inf:ViewModelChangeDispatcher Source="{Binding xxxxxAnimation}"
OnSourceChangedStartStoryboard="addadfa" />

if i had a single animation....


But I pass in an enum to manage all animations.



Posted by Jones @ 27 Apr 2009 3:20 PM
Nice post...

Posted by Roboblob @ 10 Jun 2010 8:13 AM
Great post!

I planing to use this approach in my MVVM framework just with Fame element (that inherits ContentControl).

I agree that animations and transitions should definitely be outside of ViewModel and should be controlled via XAML and triggers/events.

Cheers!

© 2005 - 2014 Josh Twist - All Rights Reserved.