Search code examples
c#wpftabnavigator

C# WPF Tab Navigate to control inside of a ListBoxItem from outside of ListBox


I have a window with version text boxes, buttons, and a ListBoxthat expands and contracts. Each ListBox item has multiple text boxes. I am trying to find a way to tab navigate from outside of the the ListBoxto the first TextBox in the first item for the ListBox. I can get it to navigate to the ListBox itself and if I hit the down arrow key it will select the first item but that is clumsy. I need it to tab directly from something outside the ListBox to something inside the ListBox.

Below is some of the XAML I use for the ListBox.

                    <ListBox x:Name="add_users_listbox" Margin="2,116,-8,0" BorderThickness="0" Height="322" Padding="0,0,0,0"
                             HorizontalContentAlignment="Center" VerticalContentAlignment="Top" 
                             HorizontalAlignment="Center" VerticalAlignment="Top"
                             SelectionMode="Single"
                             IsTabStop="True"
                             TabIndex="1004"
                             Background="{x:Null}" BorderBrush="{x:Null}"
                             ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                             ScrollViewer.CanContentScroll="False"
                             ItemsSource="{Binding Add_User_Binding}"
                             SelectedIndex="{Binding Add_User_Selected_Index, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}">

                        <ListBox.Resources>
                            <Style TargetType="{x:Type ScrollBar}" BasedOn="{StaticResource ScrollBar_Rounded}"/>
                            <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource CustomListBoxItemStyle}"/>
                            <Style TargetType="{x:Type ListBox}" >
                                <Setter Property="KeyboardNavigation.TabNavigation" Value="Continue" />
                            </Style>
                        </ListBox.Resources>

                        <ListBox.ItemTemplate>

                            <DataTemplate>
                                <Grid Height="60" Background="Transparent"
                                      HorizontalAlignment="Center" VerticalAlignment="Top">

                                    <TextBox Height="26" Width="102" Padding="0,-1,0,1" Margin="2,2,0,0"
                                                 HorizontalAlignment="Left" VerticalAlignment="Top" HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
                                                 FontFamily="Segoe UI" FontSize="16" FontWeight="Bold"
                                                 TextWrapping="NoWrap" IsReadOnlyCaretVisible="True" UndoLimit="10" AllowDrop="False" MaxLines="1"
                                                 TabIndex="{Binding First_TabIndex}"
                                                 MaxLength="20"
                                                 TextChanged="first_last_textbox_TextChanged"
                                                 PreviewTextInput="first_last_textbox_PreviewTextInput"
                                                 Text="{Binding First_Textbox, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, ValidatesOnDataErrors=True}">
                                    </TextBox>

                                </Grid>
                            </DataTemplate>
                        </ListBox.ItemTemplate>

                    </ListBox>

Solution

  • So, this is likely NOT the ideal way to do this, but it works. First we add a bound tag to the TextBox within the DataTemplate

        ItemsSource="{Binding ListBox_Item_Collection}">
    
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <TextBox Text="{Binding First_Textbox, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
                             Tag="{Binding Index}">
                    </TextBox>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    

    For simplicity I bound that tag to the corresponding index in the ItemSource collection.

                    int index = Add_User_Binding.Count;
    
                    ListBox_Item_Collection.Add(new SomeDataType()
                    {
                        Index = index,
                        The_Textbox = "Stuff in TextBox",
                    });
    

    The next step is to add a KeyDown event in the user control that comes before this one. In that event we will find the element with that tag and then use the Dispatcher to focus it.

        private void Preview_TextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Tab)
            {
                string tag = "0";
                IEnumerable<TextBox> elements = FindVisualChildren<TextBox>(this).Where(x => x.Tag != null && x.Tag.ToString() == tag);
    
                foreach (TextBox element in elements)
                {
                    FocusElement(element);
                }
            }
        }
    
        public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
        {
            if (depObj != null)
            {
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
                {
                    DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                    if (child != null && child is T)
                    {
                        yield return (T)child;
                    }
    
                    foreach (T childOfChild in FindVisualChildren<T>(child))
                    {
                        yield return childOfChild;
                    }
                }
            }
        }
    
        private void FocusElement(IInputElement element)
        {
            if (element != null)
            {
                Dispatcher.BeginInvoke
                (System.Windows.Threading.DispatcherPriority.ContextIdle,
                new Action(delegate ()
                {
                    Keyboard.Focus(element);
                }));
            }
        }
    

    As you can see, lots of work for something that should be much simpler.