Animating MVVM ViewModel transitions using VisualStateManager -- Animations not running

1.3k views Asked by At

I'm working on my first WPF project, based on the MVVM Light toolkit. The main View holds an arbitrary ViewModel in a databound ContentControl, like so:

<ContentControl x:Name="ViewModelContent" Content="{Binding CurrentViewModel}" ... />

What I'd like to do is, whenever this ViewModel changes, fade out the old ViewModel (that is, the ContentControl) and then fade in the new. This is not a new idea in the WPF world, and I've spent a fair amount of time researching how to accomplish it (both here and on other sites). I'm trying to keep this relatively simple, and what I've gathered is that I can use the VisualStateManager to define two states on the View's DataTemplate:

  • ContentChanging: The faded-out state
  • ContentChanged: The faded-in state

Then, using a GoToStateAction, I can animate to the appropriate state as needed. In my case, "as needed" is accomplished by a CurrentViewModelChanging property defined on my ViewModel (remember, I'm using MVVM Light):

private bool _vmChanging;
public bool CurrentViewModelChanging
{
    get
    {
        return _vmChanging;
    }
    private set
    {
        Set(() => CurrentViewModelChanging, ref _vmChanging, value);
    }
}

I can then utilize a DataTrigger to bind to this property and change the state accordingly. In the XAML for the DataTemplate it looks like this:

<i:Interaction.Triggers>
    <ei:DataTrigger Binding="{Binding CurrentViewModelChanging, Mode=OneWay}" Value="True">
        <ei:GoToStateAction StateName="ContentChanging"/>
    </ei:DataTrigger>
    <ei:DataTrigger Binding="{Binding CurrentViewModelChanging, Mode=OneWay}" Value="False">
        <ei:GoToStateAction StateName="ContentChanged"/>
    </ei:DataTrigger>
</i:Interaction.Triggers>

So far, so good. I put a button on my form that serves no other purpose than to toggle CurrentViewModelChanging (and thus change the state), and it does exactly what I expected: clicking it once fades out the View, and clicking it again fades it back in. The problem occurs when I try to accomplish the fade out/in when actually changing the ViewModel. Here is the code snippet where I try this:

public Standards.StandardsViewModel CurrentViewModel
{
    ....
    private set
    {
        ....
        CurrentViewModelChanging = true;
        Set(() => CurrentViewModel, ref _viewModel, value);
        CurrentViewModelChanging = false;
        ....
    };
}

What happens when this runs is essentially nothing: the ViewModel switches instantaneously without any animation. I was under the impression that changing CurrentViewModelChanging would cause the animation to run and stop any remaining code from executing until the animation was finished. That doesn't appear to be the case, so could somebody clue me in to what's happening (and how to fix it)? My best guess is that the animations run in a different thread than the executing logic and that said logic executes so quickly that there's no time for the animations to actually do anything. However, if I put a Thread.Sleep() call within the CurrentViewModelChanging toggle then I still don't get an animation: the program just hangs for however long I told it to and then immediately changes the ViewModel.

Also, in case it wasn't evident, this is all based on a DataTemplate, so solutions requiring me to employ UserControls aren't ideal. If that's what it takes then I'll certainly do it, however. Here's more complete XAML for my DataTemplate, if it helps. Note that I've left out the details of the animations in order to keep things concise:

<DataTemplate DataType="{x:Type localVMS:StandardsModuleViewModel}">
    <DockPanel x:Name="ModLayout" LastChildFill="True" Margin="0" Grid.Column="1">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ContentPresentationStates">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0:0:0.1" To="ContentChanging"/>
                    <VisualTransition GeneratedDuration="0:0:0.1" To="ContentChanged"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="ContentChanging" ... />
                <VisualState x:Name="ContentChanged" .... />
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Grid x:Name="MainMenuGrid" VerticalAlignment="Top" Background="Black" DockPanel.Dock="Top">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <ItemsControl x:Name="MainMenu" HorizontalAlignment="Left" VerticalAlignment="Bottom" ItemsSource="{Binding MainMenu}" ItemTemplate="{DynamicResource TopMenuItem}" ItemsPanel="{DynamicResource HorizontalMenuTemplate}" FontFamily="Calibri" FontSize="16" MinHeight="20" Background="Transparent" VerticalContentAlignment="Center" HorizontalContentAlignment="Center"/>
        </Grid>
        <ItemsControl x:Name="SubMenu" ItemsSource="{Binding SubMenu}" ItemsPanel="{DynamicResource HorizontalMenuTemplate}" ItemTemplate="{StaticResource SubMenuItem}" DockPanel.Dock="Top" Height="25" VerticalContentAlignment="Center" Background="White" FontFamily="Calibri" FontSize="13.333"/>
        <Grid x:Name="ViewModelLayout" DockPanel.Dock="Bottom">
            <i:Interaction.Triggers>
                <ei:DataTrigger Binding="{Binding CurrentViewModelChanging, Mode=OneWay}" Value="True">
                    <ei:GoToStateAction StateName="ContentChanging"/>
                </ei:DataTrigger>
                <ei:DataTrigger Binding="{Binding CurrentViewModelChanging, Mode=OneWay}" Value="False">
                    <ei:GoToStateAction StateName="ContentChanged"/>
                </ei:DataTrigger>
            </i:Interaction.Triggers>
            <ContentControl x:Name="ViewModelContent" Content="{Binding CurrentViewModel}" ContentTemplateSelector="{StaticResource GenericTemplateSelector}" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="10" HorizontalContentAlignment="Center" Margin="10" RenderTransformOrigin="0.5,0.5" ... />
        </Grid>
    </DockPanel>
</DataTemplate>
1

There are 1 answers

0
Mark Feldman On

The problem is that your ContentControl can only bind to one thing at a time. As soon as you change your view model the binding to the old one no longer exists, so there's nothing to fade. To fix this you'll either have to set up two overlapping UserControls and fade them in/out separately, or use a helper class to take care of it for you.