Podivuhodný případ poskakujících popisků app bar buttonů

WinUI XAML

7 years ago

Většina UWP aplikací s výhodou využije ovladací prvek CommandBar pro přehlednou prezentaci dostupných akcí uživateli. Bohužel způsobuje tento prvek někdy neočekávané chování. Dva z těchto problémů si ukážeme v tomto článku.

Akce na levé straně panelu

Ve výchozím stavu je CommandBar připraven zobrazovat akce na pravé straně panelu. Díky tomu také mohou na menších displejích příkazy "přetéci" do sekundárnho menu (funkcionalita DynamicOverflow, která byla přidána ve Windows 10 Anniversary Update). Někdy však můžete chtít zobrazit příkazy také na levé straně ovladacího prvku (např. pro globální příkazy nebo speciální úkony). CommandBar má vlastnost Content ve které můžeme definovat, co bude zobrazeno nalevo. Můžeme zde deklarovat jakýkoliv markup, ale v tomto případě budeme chtít použít tlačítka AppBarButton. Výsledek může vypadat takto:

<CommandBar>
    <CommandBar.Content>
        <StackPanel Orientation="Horizontal">
            <AppBarButton Icon="Accept" Label="Accept" />
            <AppBarButton Icon="Delete" Label="Delete" />
        </StackPanel>
    </CommandBar.Content>
</CommandBar>

Když aplikaci spustíme, první problém se nám ukáže okamžitě - popisky tlačítek vykukují na spodní straně panelu.

Labels sticking out

Vykukující popisky

Důvod je jednoduchý - výchozí rozložení talčítek je takto navrženo - popisky jsou prostě umístěny tak, že v této situaci jsou částečně vidět. Když je tlačítko v kolekci PrimaryCommands CommandBaru, ovladací prvek sám zajistí skrytí popisků když je command bar v normálním stavu a zobrazí je pouze, když je otevřen. Naštěstí je toto chování snadné napodobit nabindováním vlastnosti IsCompact tlačítek na inverzní hodnotu vlastnosti IsOpen command baru.

<CommandBar x:Name="CommandBar">
    <CommandBar.Content>
        <StackPanel Orientation="Horizontal">
            <AppBarButton Icon="Accept" Label="Accept" 
                IsCompact="{Binding ElementName=CommandBar, Path=IsOpen, Converter={StaticResource InverseConverter}}" />
            <AppBarButton Icon="Delete" Label="Delete" 
                IsCompact="{Binding ElementName=CommandBar, Path=IsOpen, Converter={StaticResource InverseConverter}}"  />
        </StackPanel>
    </CommandBar.Content>
</CommandBar>

InverseConverter je jednoduchý IValueConverter který obrací pravdivostní hodnotu.

Poskakující (a mizející) popisky

Příkazy nalevo nyní vypadají a fungují skvěle. Alespoň většinou. Ale ne vždy! Přibližně jednou za deset pokusů narazíme na to, že se popisky talčítek zobrazí napravo, nebo se dokonce nezobrazí vůbec.

When label business goes wrong

Když popisky začnou zlobit

Tento problém je velmi zvláštní a trvalo mi nějakou dobu, než se mi podařilo objevit řešení. Po delším hledání jsem narazil na odpověď na Stack Overflow od vývojáře v Microsoftu - Jay Zua, která právě tento problém adresuje. Anniversary update Windows 10 představil novou vlastnost LabelPosition, která určuje, kde bude popisek tlačítka zobrazen. Ve výchozím stavu se tlačítko podívá výše na svého otce (kterým by měl být command bar) a zkontroluje jeho vlastnost DefaultLabelPosition, který může nabývat hodnot Bottom, Right nebo Collapsed . Tento proces funguje skvěle, když tlačítka příkazů jsou uvnitř kolekce PrimaryCommands, ale jakmile jsou umístěna uvnitř jiného ovladacího prvku jakým je například právě StackPanel, začnou se chovat nepředvídatelně. Řešení problému vyžaduje úpravu výchozího stylu app bar tlačítek a zakomentování visual state pro umístění Right a Collapsed a souvisejího resource uvnitř stylu:

<Style x:Key="BottomLabelAppBarButtonStyle" TargetType="AppBarButton">
    ...
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="AppBarButton">
                <Grid x:Name="Root" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" MaxWidth="{TemplateBinding MaxWidth}" MinWidth="{TemplateBinding MinWidth}">
                    <!--<Grid.Resources>
                    <Style x:Name="LabelOnRightStyle" TargetType="AppBarButton">
                        <Setter Property="Width" Value="NaN"/>
                    </Style>
                </Grid.Resources>-->
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="ApplicationViewStates">
                            ...
                            <!--<VisualState x:Name="LabelOnRight">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Margin" Storyboard.TargetName="ContentViewbox">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="12,14,0,14"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="MinHeight" Storyboard.TargetName="ContentRoot">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource AppBarThemeCompactHeight}"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Grid.Row)" Storyboard.TargetName="TextLabel">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="0"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Grid.Column)" Storyboard.TargetName="TextLabel">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="1"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="TextAlignment" Storyboard.TargetName="TextLabel">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="Left"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Margin" Storyboard.TargetName="TextLabel">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="8,15,12,17"/>
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>
                        <VisualState x:Name="LabelCollapsed">
                            <Storyboard>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="MinHeight" Storyboard.TargetName="ContentRoot">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource AppBarThemeCompactHeight}"/>
                                </ObjectAnimationUsingKeyFrames>
                                <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="TextLabel">
                                    <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed"/>
                                </ObjectAnimationUsingKeyFrames>
                            </Storyboard>
                        </VisualState>-->
                        ...                         
                    </VisualStateManager.VisualStateGroups>
                    ...
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Zdrojový kód

Ukázkový zdrojový kód pro tento článek je dostupný na mém GitHubu.

Shrnutí

CommandBar je snadno přizpůsobitelný, ale jeho použití způsobem, na který nebyl navržen může mít nečekané následky. Tyto situaci lze sice vyřešit poměrně přímočaře, ale musíme si jich přesto být při vývoji vědomi.