WPF tab order with custom controls?

ElHaix picture ElHaix · Jan 18, 2011 · Viewed 9k times · Source

I have a WPF page that contains several out of the box controls with the tab order set.

I have a custom control (NumericSpinner) that contains: border/grid/text box/2 Repeatbuttons (up/down).

Two issues:

1) when I am in the textbox for the custom selector control, I cannot tab out of it to other controls on the page. However after clicking on one of the up/down arrows, I am able to tab over to other controls.

2) I am unable to tab into the textbox of the custom control in order. It is only after I have tabbed through all of the controls, that the cursor lands in the text box (and cannot tab out).

Context:

<ComboBox Margin="97,315,21,0" Name="txtdweldatcdu" Style="{StaticResource fieldComboBoxStyle}" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" VerticalAlignment="Top" TabIndex="10" />
    <WpfControls:NumericSpinner Margin="97,338,21,0" Name="txtdweldatpctcomplete" HorizontalAlignment="Left" VerticalAlignment="Top" AllowNegativeValues="True" MaxValue="100" TabIndex="11" />
    <ComboBox Margin="97,363,21,0" Name="txtdweldatclass" Style="{StaticResource fieldComboBoxStyle}" IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left" VerticalAlignment="Top" TabIndex="12" />

Portion of the custom control:

 <Border BorderThickness="1" BorderBrush="Gray" Margin="0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="20" Width="117">
        <Grid Margin="0">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="98"/>
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBox Name="valueText" 
                     BorderThickness="0" 
                     Grid.RowSpan="2"
                     Style="{StaticResource spinnerTextBoxStyle}"
                     PreviewKeyDown="valueText_PreviewKeyDown"
                     PreviewTextInput="valueText_PreviewTextInput"
                     TextChanged="valueText_TextChanged"
                     IsReadOnly="{Binding ElementName=Spinner, Path=IsReadOnly}"
                     Text="{Binding ElementName=Spinner, Path=Value, Mode=TwoWay}"
                     KeyboardNavigation.IsTabStop="True"
                     AcceptsTab="True"/>
            <RepeatButton Name="upButton" Style="{StaticResource spinnerRepeatButtonStyle}" Click="upButton_Click"  Grid.Column="1" Grid.Row="0" Height="10" Width="18" VerticalAlignment="Top" HorizontalAlignment="Right" HorizontalContentAlignment="Center">
                <Polygon  HorizontalAlignment="Center" Points="3,2 2,3 4,3"  Fill="Black"  Stretch="Uniform"  Stroke="Black"  StrokeThickness="0" />
            </RepeatButton>
            <RepeatButton Name="downButton" Style="{StaticResource spinnerRepeatButtonStyle}" Click="downButton_Click"  Grid.Column="1" Grid.Row="1" Height="10" Width="18" VerticalAlignment="Top" HorizontalAlignment="Right" HorizontalContentAlignment="Center">
                <Polygon  HorizontalAlignment="Center" Points="2,2 4,2 3,3"  Fill="Black"  Stretch="Uniform"  Stroke="Black"  StrokeThickness="0" />
            </RepeatButton>
        </Grid>
    </Border>

The custom control consists of the xaml and code-behind file.

Parent xaml page, containing all controls is loaded dynamically and contains no code-behind.

In the constructor for the custom control, I have set the following as a test:

    valueText.TabIndex = 3;
    this.TabIndex = 3;

The fourth time I tab, I actually get the cursor into the text field, however I cannot tab out of it.

With this in mind, the first step would be to create a control parameter that I can pass a tab order number that would get set in the control's codebehind.

I created a CustomTabIndex property:

/// <summary>
/// Custom tab index property
/// </summary>
public int CustomTabIndex
{
    get { return (int)GetValue(CustomTabIndexProperty); }
    set { SetValue(CustomTabIndexProperty, value); }
}

public static readonly DependencyProperty CustomTabIndexProperty = 
    DependencyProperty.Register("CustomTabIndex", typeof(int), typeof(NumericSpinner));

And in the xaml, when I try to set CustomTabIndex="3", I receive the error:

The property 'CustomTabIndex' was not found in type 'NumericSpinner'.

Some assistance would be appreciated.

Answer

HaxElit picture HaxElit · Jan 20, 2011

I have an answer to the first one... In your static constructor for your CustomControl add the following

KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));

That should let you tab in and out of the control and allow you to set a tab index for each of the children of your custom control.

As for your second question, I'm working on figuring out the same thing. I think it has to due with the fact that your custom control has Focusable = False. But setting this to true will make the control get focus not the actual children. I think what might be required is an event handler on your CustomControl for the GotFocus event. When the control gets focus then do a

this.MoveFocus(new TraversalRequest(FocusNavigationDirection.First));

and that should move the focus to the child elements.

I'll post a follow up when I figure out the proper way of doing it.

Update

So for your second point.

Make sure you set focusable to false and the TabNavigationProperty like so in the static constructor for your control

FocusableProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(false));
KeyboardNavigation.TabNavigationProperty.OverrideMetadata(typeof(NumericSpinner), new FrameworkPropertyMetadata(KeyboardNavigationMode.Local));

This will allow the control to work as expected and respect the Tab Order on the control and also all the children. Make sure that in your style you don't set the Focusable or TabNavigation properties.

Raul