Nekonzistentní chování Resources v ItemTemplate pro Anniversary Update

WinUI XAML

7 years ago

Zdá se, že Windows 10 Anniversary Update obsahuje skrytou chybu ve zpracování Resources unvitř ItemTemplate pro položky seznamů. Narazil jsem na ni při práci na UWP aplikaci a popíšu problém samotný spolu s řešením, kterým můžete zajistit, že se vaše aplikace bude chovat správně na všech verzích Windows 10.

Problém

Předpokládejme že chceme vytvořit seznam položek následujícího typu:

public class AnimatableItem : INotifyPropertyChanged
{
    private bool _isAnimating = false;

    public event PropertyChangedEventHandler PropertyChanged;

    public bool IsAnimating
    {
        get { return _isAnimating; }
        set
        {
            _isAnimating = value;
            OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Budeme používat vlastnost IsAnimating abychom určili, kdy se budou jednotlivé položky seznamu animovat. Nyní implementujeme ListView , který položky zobrazí:

<ListView ItemsSource="{x:Bind Items}">
     <ListView.ItemTemplate>
         <DataTemplate>
             <Border x:Name="Container">
                 <Border.Background>
                     <SolidColorBrush Color="Transparent" />
                 </Border.Background>
                 <Border.Resources>
                     <Storyboard x:Key="BlinkingAnimation" RepeatBehavior="Forever" AutoReverse="True">
                         <ColorAnimation Duration="0:0:1" To="Red" Storyboard.TargetProperty="(Grid.Background).(SolidColorBrush.Color)" Storyboard.TargetName="Container">
                         </ColorAnimation>
                     </Storyboard>
                  </Border.Resources>
                  <StackPanel VerticalAlignment="Center">
                     <TextBlock Text="Is animating:" FontWeight="Bold" HorizontalAlignment="Center" />
                     <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding IsAnimating}" />
                   </StackPanel>
                   <interactivity:Interaction.Behaviors>
                       <core:DataTriggerBehavior 
                                Binding="{Binding IsAnimating, Mode=OneWay}" 
                                ComparisonCondition="Equal"
                                Value="true">
                                <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource BlinkingAnimation}" />
                       </core:DataTriggerBehavior>
                       <core:DataTriggerBehavior 
                                Binding="{Binding IsAnimating, Mode=OneWay}" 
                                ComparisonCondition="Equal"
                                Value="false">
                                <media:ControlStoryboardAction ControlStoryboardOption="Stop" Storyboard="{StaticResource BlinkingAnimation}" />
                        </core:DataTriggerBehavior>
                 </interactivity:Interaction.Behaviors>
             </Border>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Je nutné podotknout dvě důležité věci o ItemTemplate našeho ListView :

  • Do Resources jsme přidali Storyboard BlinkingAnimation . Tato animace zajistí, že bude pozadí položky seznamu blikat červeně.

  • Každá položka obsahuje DataTriggerBehavior a ControlStoryboardAction , které jsou součástí balíčku XAML Behaviors (zde na GitHubu). Pomocí nich chceme spouštět animaci právě když se změní hodnota vlastnosti IsAnimating

    Očekáváme, že po spuštění aplikace a nastavení Items[2].IsAnimating = true začne třetí položka seznamu blikat červeně. Překvapivě to však v případě Anniversary Update není pravda.

Jak můžete vidět, jak November Update (build 10586) tak Creators Update (build 15063) pracují správně. U Anniversary Update se ale vlastnost IsAnimating nastaví správně u třetí položky, ale animace se spustí na první položce seznamu.

Experimenty

Vyzkoušel jsem některé experimenty, abych zjistil, co se přesně odehrává. Nezáleží na SDK, pro které aplikaci zkompilujeme. Aplikace se vždy chová stejně bez ohledu na SDK, jediné co rozhoduje je verze Windows 10, na které běží. Chování je stejné pro Mobile i Desktop SKU. Pokud zkusíme debugovat ControlStoryboardAction , zjistíme, že u Anniversary Update je instance Storyboardu, který je nastaven jeho stejnojmenné vlastnosti pomocí výrazu StaticResource je skutečně vždy instancí první položky seznamu. Přesto však každá položka vytváří svou vlastní unikátní instanci, která však není použita. Je zřejmé, že v Anniversary Update systém zpracovává Resources v DataTemplate trochu jinak než v ostatních verzích Windows 10. Protože se "správné" chování v Creators Update vrátilo, lze pravděpodobně bezpečně říci, že jde o nedokumentovaný bug.

Řešení

Naštěstí je velmi snadné problém obejít. Můžeme zabalit obsah ItemTemplate do UserControlu a použít jej namísto přímého obsahu. Nejprve do projektu přidáme nový UserControl s následujícím XAML kódem:

<UserControl
    x:Class="AnimationInDataTemplateProblem.AnimationItemControl"
    ...>
    <Border BorderBrush="Black" BorderThickness="1" Width="100" Height="100" x:Name="Container">
        <Border.Background>
            <SolidColorBrush Color="Transparent" />
        </Border.Background>
        <Border.Resources>
            <Storyboard x:Key="BlinkingAnimation" RepeatBehavior="Forever" AutoReverse="True">
                <ColorAnimation Duration="0:0:1" To="Red" Storyboard.TargetProperty="(Grid.Background).(SolidColorBrush.Color)" Storyboard.TargetName="Container">
                </ColorAnimation>
            </Storyboard>
        </Border.Resources>
        <StackPanel VerticalAlignment="Center">
            <TextBlock Text="Is animating:" FontWeight="Bold" HorizontalAlignment="Center" />
            <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding IsAnimating}" />
        </StackPanel>
        <interactivity:Interaction.Behaviors>
            <core:DataTriggerBehavior 
                Binding="{Binding IsAnimating, Mode=OneWay}" 
                ComparisonCondition="Equal"
                Value="true">
                <media:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource BlinkingAnimation}" />
            </core:DataTriggerBehavior>
            <core:DataTriggerBehavior 
                Binding="{Binding IsAnimating, Mode=OneWay}" 
                ComparisonCondition="Equal"
                Value="false">
                <media:ControlStoryboardAction ControlStoryboardOption="Stop" Storyboard="{StaticResource BlinkingAnimation}" />
            </core:DataTriggerBehavior>
        </interactivity:Interaction.Behaviors>
    </Border>
</UserControl>

A použijeme jej v seznamu takto:

<ListView ItemsSource="{x:Bind Items}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <local:AnimationItemControl />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Všimněme si, že data binding pomocí syntaxe Binding nadále funguje, protože UserControl používá aktuální DataContext . Pokud chceme použít x:Bind syntaxi, musíme vytvořit manuálně novou dependency property, ke které budeme bindovat. Tato jednoduchá změna zajistí, že animace bude fungovat správně ve všech verzích Windows 10. Následující obrázky porovnávají najednou obě implementace:

Zdrojový kód

Ukázkový zdrojový kód k tomuto článku je dostupný na mém GitHubu.

Shrnutí

Ukázali jsme si, že Anniversary Upate nepracuje s Resource uvnitř položek seznamů správně. Přestože jde nyní (duben 2017) o nejpoužívanější verzi Windows 10, zdá se, že tento problém zatím nebyl nikde dokumentován. Naštěstí pro nás vývojáře je možné chybu snadno obejít zabalením obsahu ItemTemplate do UserControlu , čímž vynutíme správné chování ve všech verzích Windows 10.