WinRT - CacheMode et VisualStatesTransition

4 minutes read

Je suis récemment tombé sur un bug en portant mon jeu de reversi fonctionnant sur Wp7, Silverlight et WPF vers WinRT. En effet les animations qui permettaient aux pions de se retourner ne se lançaient pas sur WinRT. Ces animations sont en fait des transitions entre deux états visuels dont le code était strictement identique sur toutes les plateformes. Après quelques jours de galères et de recherche j’ai fini par comprendre que le problème résidait d’un cache que j’avais mis sur le contrôle Board contenant mes Tokens. Ce contrôle Board n’est qu’un contrôle personnalisé héritant de ItemsControl. Dans le template de ma board j’avais placé un CacheMode à BitmapCache.

A des fins d’exemple j’ai reproduit le comportement dans un petit projet contenant une application WPF et une application WinRT.

Le contrôle Token est le suivant. Lorsque sa propriété IsBlack change, une animation est lancée :

#if NETFX_CORE
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Shapes;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;
#endif

namespace CacheBug
{
    [TemplatePart(Name = PartWhite, Type = typeof(Ellipse))]
    [TemplatePart(Name = PartBlack, Type = typeof(Ellipse))]
    public sealed class TokenControl : Control
    {
        private const string PartWhite = "PART_White";
        private const string PartBlack = "PART_Black";

        private Ellipse _black = null;
        private Ellipse _white = null;

        public TokenControl()
        {
            this.DefaultStyleKey = typeof(TokenControl);
        }

        public bool IsBlack
        {
            get { return (bool)GetValue(IsBlackProperty); }
            set { SetValue(IsBlackProperty, value); }
        }

        public static readonly DependencyProperty IsBlackProperty =
            DependencyProperty.Register("IsBlack", typeof(bool), typeof(TokenControl), new PropertyMetadata(true, new PropertyChangedCallback((s, a) =>
                {
                    ((TokenControl)s).SetColor();
                })));

#if NETFX_CORE
        protected override void OnApplyTemplate()
#else
        public override void OnApplyTemplate()
#endif
        {
            base.OnApplyTemplate();

            _white = (Ellipse)GetTemplateChild(PartWhite);
            _black = (Ellipse)GetTemplateChild(PartBlack);

            SetColor();
        }

        private void SetColor()
        {
            if (_black == null || _white == null)
                return;

            if (IsBlack)
                VisualStateManager.GoToState(this, "BlackState", true);
            else
                VisualStateManager.GoToState(this, "WhiteState", true);
        }
    }
}

Le template du contôle est le suivant :

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CacheBug">

    <Style TargetType="local:TokenControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:TokenControl">
                    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="VisualStateGroup">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition From="BlackState" To="WhiteState">
                                        <Storyboard Duration="0:0:0.5">
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Duration="0:0:1" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.0" Value="0" />
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseOut"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                    <VisualTransition From="WhiteState" To="BlackState">
                                        <Storyboard Duration="0:0:0.5">
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Duration="0:0:1" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                            <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0" />
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.25" Value="0">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseIn"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                                <EasingDoubleKeyFrame KeyTime="0:0:0.5" Value="1">
                                                    <EasingDoubleKeyFrame.EasingFunction>
                                                        <CubicEase EasingMode="EaseOut"/>
                                                    </EasingDoubleKeyFrame.EasingFunction>
                                                </EasingDoubleKeyFrame>
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </VisualTransition>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="BlackState">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="WhiteState">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_Black" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="0" />
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames BeginTime="0:0:0" Storyboard.TargetName="PART_White" Storyboard.TargetProperty="(UIElement.RenderTransform).ScaleX">
                                            <EasingDoubleKeyFrame KeyTime="0:0:0" Value="1" />
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Ellipse x:Name="PART_Black" RenderTransformOrigin="0.5,0.5" Fill="Black" CacheMode="BitmapCache">
                            <Ellipse.RenderTransform>
                                <ScaleTransform />
                            </Ellipse.RenderTransform>
                        </Ellipse>
                        <Ellipse x:Name="PART_White" RenderTransformOrigin="0.5,0.5" Fill="#FFFAFAFA" CacheMode="BitmapCache">
                            <Ellipse.RenderTransform>
                                <ScaleTransform />
                            </Ellipse.RenderTransform>
                        </Ellipse>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

La page de test est la suivante (vous remarquerez la ligne en commentaire pour qui provoque le bug si décommentée) :

<Page
    x:Class="CacheBug.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CacheBug"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Page.Resources>
        <Style x:Key="ItemsControlStyle" TargetType="ItemsControl">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ItemsControl">
                        <Border Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}"
                                Padding="{TemplateBinding Padding}">
                            <!-- Uncomment below to make tokens stop animating -->
                            <!--<Border.CacheMode>
                                <BitmapCache />
                            </Border.CacheMode>-->
                            <ItemsPresenter />
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Page.Resources>

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <ItemsControl x:Name="lstBox" Style="{StaticResource ItemsControlStyle}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <local:TokenControl Width="100" Height="100" IsBlack="{Binding IsBlack}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Page>

Et son codebehind se charge d’initialiser une collection de pions et de changer la valeur de la propriété IsBlack avec un timer :

using System;
using System.Collections.Generic;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace CacheBug
{
    public sealed partial class BlankPage : Page
    {
        private readonly DispatcherTimer _timer = new DispatcherTimer();
        private readonly Random _rnd = new Random();
        private readonly List<TokenModel> list = new List<TokenModel>();

        public BlankPage()
        {
            this.InitializeComponent();

            _timer.Tick += TimerTick;
            _timer.Interval = TimeSpan.FromSeconds(0.5d);
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });
            list.Add(new TokenModel { IsBlack = false });
            list.Add(new TokenModel { IsBlack = true });

            lstBox.ItemsSource = list;

            _timer.Start();
        }

        private void TimerTick(object sender, object e)
        {
            int index = _rnd.Next(0, list.Count - 1);
            var token = list[index];
            token.IsBlack ^= true;
        }
    }
}

Ce bug est spécifique à WinRT car le même code fonctionne correctement en WPF, Silverlight ou même WP7. J’ai donc désactivé le CacheMode sur ce contrôle pour WinRT.

Bug ou feature je ne sais pas trop mais en tout cas c’est posté sur Connect.

Et comme souvent, vous pouvez télécharger le code de sample sur mon Skydrive.

Edit :

J’ai eu une réponse du pourquoi celà ne marchait pas correctement sous WinRT avec le CacheMode. C’est dû au fait que certains types d’animations (celles impliquant un recalcul du layout ou celles que le moteur estime comme étant lourde) ne sont plus lancées automatiquement. Pour ce faire il faut rajouter la propriété EnableDependentAnimation à True sur chacune des animations. Après tout fonctionne comme prévu.

Updated:

Leave a Comment