The curious case of jumping app bar button labels

WinUI XAML

6 years ago

Most UWP apps can benefit from using the CommandBar control to present easily accessible commands to the user. Unfortunately the control sometimes behaves unexpectedly. Two of these surprising problems are addressed in this article.

Left-side commands

By default, CommandBar is designed to stack commands on the right side of the control. This allows easy overflow in case screen size is too small (DynamicOverflow feature introduced in Anniversary Update). Sometimes you might want to display commands on the left side of the control as well (for example a global command or a special action. Command bar is a content control and its Content property defines what should be displayed on the left. We can declare any markup here, but in this case we want to use AppBarButton controls. Example of this might be the following:

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

When we launch the application, we see the first problem immediately - the app bar button label is clearly sticking out from the bottom of the bar.

Labels sticking out

Labels sticking out

The reason is simple - the default layout of the app bar button is designed like this - the label is just situated in this position. When the button is part of normal PrimaryCommands collection of CommandBar, the control itself ensures to hide the label when the command bar is in normal state and display it only when it is expanded. It is quite easy to mimic this behavior by binding the IsCompact property of the AppBarButtons to the inverse of the command bar's IsOpen property.

<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>

The InverseConverter is a simple IValueConverter that negates the provided boolean value.

Labels jumping all around (and disappearing)

The left commands now look and feel great and all seems rosy. At least in most cases. Not always! In about one try out of ten, you will encounter labels of app bar buttons displaying on the right side of the control or even not being present at all!

When label business goes wrong

When label business goes wrong

This is very curious and it took me a while to dig out a reason and solution for this. After some searching around I stumbled upon a Stack Overflow answer by Microsoft's Jay Zuo, addressing exactly this problem. The Anniversary update of Windows 10 introduced a new LabelPosition property, which determines where the label should be displayed. In the default state, it looks up to the parent (which should be a command bar) and checks the DefaultLabelPosition property, which can be Bottom, Right or Collapsed . This process works great when the app bar buttons are inside the PrimaryCommands collection of CommandBar, but when they are embedded inside a different control, like a StackPanel, they tend to do whatever they want randomly. Solution requires us to edit the default app bar button style and comment out the visual states for Right and Collapsed placement, including a related resource:

<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>

Source code

Example source code for this article is available on my GitHub.

Summary

CommandBar is quite customizable, but using it in ways it was not directly designed for may lead to unexpected and unpredictable results. These situations can be resolved using straightforward workarounds, but we must still be aware of them while building your app.