Search code examples
c#animationuwpuwp-xamlargumentexception

UWP Connected Animation Crashes After Second Use


I've connected two forms with a click event that triggers a connected animation in both directions. The first time going forward and back it works fine. The second time forward it works, but trying to go back a second time causes the app to crash with the following exception:

System.ArgumentException: The parameter is incorrect. Cannot start animation - the source element is not in the element tree.

This occurs on the first line of SecondPage_BackRequested, but only on the second execution. The first execution works and animates perfectly.

Any help would be greatly appreciated. I have poured through the connected animation documentation, and as far as I can tell this is how it is supposed to be used, but I cannot find a reference to this error occurring anywhere.

My Code (MainPageViewModel omitted as it's not relevant, but can be added on request):

MainPage.xaml

<Page
    x:Class="AnimTest.Views.Main.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:models="using:AnimTest.Models"
    xmlns:main="using:AnimTest.Views.Main"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
          Padding="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0"
                   Style="{ThemeResource HeaderTextBlockStyle}"
                   Text="AnimTest"/>
        <GridView x:Name="TileGrid"
                  Grid.Row="1"
                  IsItemClickEnabled="True"
                  ItemsSource="{x:Bind ViewModel.Tiles, Mode=OneWay}"
                  ItemClick="GridView_ItemClick"
                  Loaded="TileGrid_Loaded">
            <GridView.ItemTemplate>
                <DataTemplate x:DataType="models:Tile">
                    <Border x:Name="TileBorder"
                            Background="Red"
                            MinHeight="150"
                            MinWidth="200">
                        <StackPanel Orientation="Vertical"
                                    VerticalAlignment="Center">
                            <SymbolIcon Symbol="World"/>
                            <TextBlock Text="{x:Bind Name}"
                                       HorizontalTextAlignment="Center"/>
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </GridView.ItemTemplate>
        </GridView>
    </Grid>
</Page>

MainPage.xaml.cs

public sealed partial class MainPage : Page
{
    public MainPageViewModel ViewModel => (MainPageViewModel)DataContext;

    public MainPage()
    {
        InitializeComponent();

        DataContext = new MainPageViewModel();
    }

    private void GridView_ItemClick(object sender, ItemClickEventArgs e)
    {
        TileGrid.PrepareConnectedAnimation("borderIn", e.ClickedItem, "TileBorder");
        Frame.Navigate(typeof(SecondPage));
    }

    private async void TileGrid_Loaded(object sender, RoutedEventArgs e)
    {
        var animation = ConnectedAnimationService.GetForCurrentView().GetAnimation("borderOut");
        if (animation != null)
        {
            var success = await TileGrid.TryStartConnectedAnimationAsync(animation, ViewModel.Tiles[0], "TileBorder");
        }
    }
}

SecondPage.xaml

<Page
    x:Class="HomeTiles.Views.Thermostat.ThermostatPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:HomeTiles.Views.Thermostat"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Border x:Name="MainBorder"
            Background="Red">
    </Border>
</Page>

SecondPage.xaml.cs

public sealed partial class SecondPage : Page
{
    public SecondPage()
    {
        this.InitializeComponent();
        SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
        SystemNavigationManager.GetForCurrentView().BackRequested += SecondPage_BackRequested;
    }

    private void SecondPage_BackRequested(object sender, BackRequestedEventArgs e)
    {
        ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("borderOut", MainBorder);
        Frame?.GoBack();
        e.Handled = true;
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        var animation = ConnectedAnimationService.GetForCurrentView().GetAnimation("borderIn");
        animation?.TryStart(MainBorder);
    }
}

Solution

  • The problem is actually not with the connected animation, but with the navigation events.

    First time you reach SecondPage you hook up the BackRequested event and when you go back, everything is fine. However, the event handler stays attached to the event even after you navigate from the SecondPage. This is a problem, because once you navigate to SecondPage again, now the even will be registered twice. And also the first time the handler runs it fails, as the first handler is connected to the previous instance of the page and the connected animation has already gone through for this one. Finally - because of the event, the page will stay in memory forever, which could cause serious memory leaks.

    The solution is quite easy - you must make sure you don't forget to unsubscribe the even handler when you are leaving the page, for example in the OnNavigatedFrom method and subscribe in the OnNavigatedTo method for better clarity:

    public sealed partial class SecondPage : Page
    {
        public SecondPage()
        {
            this.InitializeComponent();
    
        }
    
        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            SystemNavigationManager.GetForCurrentView().BackRequested -= SecondPage_BackRequested;
        }
    
        private void SecondPage_BackRequested(object sender, BackRequestedEventArgs e)
        {
            ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("borderOut", MainBorder);
            Frame?.GoBack();
            e.Handled = true;
        }
    
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
            SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
            SystemNavigationManager.GetForCurrentView().BackRequested += SecondPage_BackRequested;
    
            var animation = ConnectedAnimationService.GetForCurrentView().GetAnimation("borderIn");
            animation?.TryStart(MainBorder);
        }
    }
    

    To avoid this kind of problem I usually set up the BackRequested event for the whole application in the App and subscribe it only once at launch. You can then put the connected animation code in the OnNavigatedFrom method instead of having to subscribe to BackRequested:

    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
        base.OnNavigatedFrom(e);       
        ConnectedAnimationService.GetForCurrentView().PrepareToAnimate("borderOut", MainBorder);
    }