Design Question - keyboard navigation

Topics: Prism v4 - WPF 4
Mar 25, 2013 at 9:09 PM
I am designing an application to be used in the livingroom from the couch, so lots of big buttons and minimal mouse interaction. I have decided to use prism as it allows me to easily add features in a modular way and forces me to use design patterns that should save me from big headaches down the road. I am fairly new to using prism but I have managed to get a basic interface down along with navigation working. When the program starts it loads the MainMenuModule into the maincontent region. The MainMenuModule hosts the mainmenuregion which is an items control region. There are two modules that load a view with a button into the mainmenuregion. when a button is clicked it fires the appropriate requestnavigate to switch out the maincontent region. There is also a back button in the shell that allows the user to go back a view. All of this functionality works fine. I would now like to adapt my current solution to work using the arrow keys and enter to navigate and the esc key mapped to the back button. For navigation to work I need to have the main menu region select the first item in the list (highlight for the user) and when the user presses an arrow key the appropriate control (most likely a button) is selected, the user can then press enter to fire the click event on the button resulting in the next view appearing. (Experience should be similar to the Media Center program of windows 7) I have uploaded a copy of my current solution to my dropbox here.

Any help, even just a point in the right direction would be greatly appreciated.

Thanks!
Mar 26, 2013 at 8:45 PM
So I have switched from using an itemscontrol to using a ListBox for my menu regions. This allows me to setup the navigation easily enough but now I cannot figure out how to get the menu items to show up in one row. Each item is shown as a separate row. Here is the MainMenuRegion
    <ListBox Name="MainMenuListBox" prism:RegionManager.RegionName="{x:Static inf:RegionNames.MainMenu}">
        <ListBox.ItemsPanel>           
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"  IsItemsHost="True" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
The two views that are loaded are identical except for the image and text that is displayed so I will show just one. Here is the view xaml for the Movie Module.
<ListBoxItem x:Class="MovieModule.View.MainMenuButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" Style="{DynamicResource MovieMainMenuItemStyle}" Content="{Binding ButtonText}" />
And here is the MovieMainMenuItemStyle and template
    <Style x:Key="MovieMainMenuItemStyle" TargetType="ListBoxItem">
        <Setter Property="FocusVisualStyle" Value="{DynamicResource NuclearButtonFocusVisual}" />
        <Setter Property="Background" Value="{DynamicResource NormalBrush}" />
        <Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
        <Setter Property="BorderBrush" Value="{DynamicResource NormalBorderBrush}" />
        <Setter Property="Margin" Value="20,0,20,0" />
        <Setter Property="Template" Value="{DynamicResource MovieMainMenuItemTemplate}" />
    </Style>

    <ControlTemplate x:Key="MovieMainMenuItemTemplate" TargetType="{x:Type ListBoxItem}">
        <ControlTemplate.Resources>
            <Storyboard x:Key="HoverOn">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverShineBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="1" />
                </DoubleAnimationUsingKeyFrames>
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="1" />
                </DoubleAnimationUsingKeyFrames>

            </Storyboard>
            <Storyboard x:Key="HoverOff">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverShineBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>

            </Storyboard>
            <Storyboard x:Key="PressedOn">

                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="PressedBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="1" />
                </DoubleAnimationUsingKeyFrames>

            </Storyboard>
            <Storyboard x:Key="PressedOff">

                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="PressedBorder"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>

            </Storyboard>
            <Storyboard x:Key="FocusedOn">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="FocusVisualElement"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="1" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="FocussedOff">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="FocusVisualElement"
                                               Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>

        </ControlTemplate.Resources>
        <Grid x:Name="Grid" Width="250" Height="250" Margin="10" >
            <Border x:Name="Background" Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" />
            <Border x:Name="HoverBorder" Opacity="0" Background="{StaticResource HoverBrush}"
                    BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" />
            <Border x:Name="HoverShineBorder" Opacity="0" Background="{StaticResource HoverShineBrush}"
                    BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" />
            <Border x:Name="PressedBorder" Opacity="0" BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Background="{StaticResource PressedBrush}" />
            <Border x:Name="ShineBorder" BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3" Background="{StaticResource ShineBrush}"
                    Opacity="1" />

            <StackPanel Margin="30,10,30,0" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Orientation="Vertical"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}" >
                <Image Stretch="UniformToFill" HorizontalAlignment="Left">
                    <Image.Source>
                        <BitmapImage UriSource="Images/movieIcon.png" />
                    </Image.Source>
                </Image>
                <TextBlock Margin="0,20,0,10" Text ="{TemplateBinding Content}" TextAlignment="Center"/>

            </StackPanel>
            <Border x:Name="FocusVisualElement" IsHitTestVisible="false" BorderBrush="{StaticResource HoverShineBrush}"
                    BorderThickness="1" CornerRadius="3,3,3,3" Margin="1,1,1,1" Opacity="0" />
        </Grid>
        <ControlTemplate.Triggers>
            <Trigger Property="IsSelected" Value="true">
                <Setter TargetName="Background" Property="Effect" >
                    <Setter.Value>
                        <DropShadowEffect Color="Red" BlurRadius="5" ShadowDepth="0" />
                    </Setter.Value>
                </Setter>
            </Trigger>
            <Trigger Property="IsKeyboardFocused" Value="true">
                <Trigger.ExitActions>
                    <BeginStoryboard Storyboard="{StaticResource FocussedOff}" x:Name="FocussedOff_BeginStoryboard" />
                </Trigger.ExitActions>
                <Trigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource FocusedOn}" x:Name="FocusedOn_BeginStoryboard" />
                </Trigger.EnterActions>

            </Trigger>
            <Trigger Property="IsMouseOver" Value="true">
                <Trigger.ExitActions>
                    <BeginStoryboard Storyboard="{StaticResource HoverOff}" x:Name="HoverOff_BeginStoryboard" />
                </Trigger.ExitActions>
                <Trigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource HoverOn}" />
                </Trigger.EnterActions>

            </Trigger>
            <Trigger Property="IsEnabled" Value="true" />
            <Trigger Property="IsEnabled" Value="false">
                <Setter Property="Background" Value="{DynamicResource DisabledBackgroundBrush}" TargetName="Background" />
                <Setter Property="BorderBrush" Value="{DynamicResource DisabledBorderBrush}" TargetName="ShineBorder" />
                <Setter Property="Opacity" TargetName="Grid" Value="0.5" />
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
All I need to do is be able to get the list items to arrange horizontally instead of vertically. I have tried using both a WrapPanel and a StackPanel for the itemspanel template and neither seems to work. Also there is small dotted line that surrounds the currently selected listitem which makes me think for whatever reason each list item is getting a max width. It would be nice if this dotted line could be removed as well but it is not really a priority at the moment.
Mar 27, 2013 at 6:51 PM
Edited Mar 27, 2013 at 6:52 PM
Hi,

Based on my understanding, PRISM does not offer any feature regarding navigation with keyboard keys. However, you can check if this functionality exist in WPF. As a starting point, WPF offers a KeyboardNavigation class which lets you define focus navigation for some specific navigation keys. You can find more information about this here:
Based on this, we could see how to adapt this to work in a PRISM scenario.

On the other hand, we could download and run your application sample, but it would be useful if you could re-upload the sample updated with the latest style changes so we can dig further into the issue you are experiencing with horizontal orientation.

Regards,

Federico Martinez
http://blogs.southworks.net/fmartinez
Mar 28, 2013 at 12:35 AM
Here is the code for the updated template
Apr 3, 2013 at 5:05 PM
Hi,

We checked your sample and we found that what was causing the problem was the Default.xaml style you use for your ListBox controls. In your MainMenu view you are defining a StackPanel on your ListBox.ItemsPanel template, which is not the same as your ListBox.Template. The ListBox is using a template from your Default style which is the following:
                <ControlTemplate TargetType="{x:Type ListBox}">
                    <Grid>
                        <Border x:Name="Border" BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2"
                                Background="{DynamicResource ControlBackgroundBrush}" />
                        <ScrollViewer Margin="1" Style="{DynamicResource NuclearScrollViewer}" Focusable="false"
                                      Background="{x:Null}">
                            <StackPanel Margin="1,1,1,1" IsItemsHost="true" />
                        </ScrollViewer>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Background" Value="{DynamicResource DisabledBackgroundBrush}"
                                    TargetName="Border" />
                            <Setter Property="BorderBrush" Value="{DynamicResource DisabledBorderBrush}"
                                    TargetName="Border" />
                        </Trigger>
                        <Trigger Property="IsGrouping" Value="true">
                            <Setter Property="ScrollViewer.CanContentScroll" Value="false" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
A simple approach on how to solve your problem could be to copy this template to your MainMenu view and add Orientation="Horizontal" to the StackPanel. If you prefer to use this orientation for all of your ListBox, then you can simply change it in your Default style.

Hope this helps,

Federico Martinez
http://blogs.southworks.net/fmartinez
Apr 4, 2013 at 12:23 AM
Awesome, thank you for the help. I will give your suggestion a try this weekend and see if it works.