Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I set the TabIndex on a WPF Expander control?

In this example window, tabbing through goes from the first textbox, to the last textbox and then to the expander header.

<Window x:Class="ExpanderTab.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    FocusManager.FocusedElement="{Binding ElementName=FirstField}">
    <StackPanel>
        <TextBox TabIndex="10" Name="FirstField"></TextBox>
        <Expander TabIndex="20" Header="_abc">
            <TextBox TabIndex="30"></TextBox>
        </Expander>
        <TextBox TabIndex="40"></TextBox>
    </StackPanel>
</Window>

Obviously, I'd like this to go First text box, expander header, then last textbox. Is there an easy way to assign a TabIndex to the header of the expander?

I've tried forcing the expander to be a tabstop using KeyboardNavigation.IsTabStop="True", but that makes the whole expander get focus, and the whole expander doesn't react to the spacebar. After two more tabs, the header is again selected and I can open it with the spacebar.

Edit: I'll throw a bounty out there for anyone who can come up with a cleaner way to do this - if not, then rmoore, you can have the rep. Thanks for your help.

like image 809
Eclipse Avatar asked Jun 12 '09 21:06

Eclipse


2 Answers

The following code will work even without the TabIndex properties, they are included for clarity about the expected tab order.

<Window x:Class="ExpanderTab.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" Height="300" Width="300" FocusManager.FocusedElement="{Binding ElementName=FirstField}">
    <StackPanel>
        <TextBox TabIndex="10" Name="FirstField"></TextBox>
        <Expander TabIndex="20" Header="Section1" KeyboardNavigation.TabNavigation="Local">
            <StackPanel KeyboardNavigation.TabNavigation="Local">
                <TextBox TabIndex="30"></TextBox>
                <TextBox TabIndex="40"></TextBox>
            </StackPanel>
        </Expander>
        <Expander TabIndex="50" Header="Section2" KeyboardNavigation.TabNavigation="Local">
            <StackPanel KeyboardNavigation.TabNavigation="Local">
                <TextBox TabIndex="60"></TextBox>
                <TextBox TabIndex="70"></TextBox>
            </StackPanel>
        </Expander>
        <TextBox TabIndex="80"></TextBox>
    </StackPanel>
</Window>
like image 92
jjxtra Avatar answered Sep 18 '22 20:09

jjxtra


I found a way, but there's got to be something better.


Looking at the Expander through Mole, or looking at it's ControlTemplate generated by Blend we can see that the header part that is responding to Space/Enter/Click/etc is really a ToggleButton. Now the bad news, Because the Header's ToggleButton has a diffrent layout for the Expander's Expanded properties Up/Down/Left/Right it's already has styles assigned to it through the Expander's ControlTemplate. That precludes us from doing something simple like creating a default ToggleButton style in the Expander's Resources.

alt text

If you have access to the code behind, or don't mind adding a CodeBehind to the Resource Dictionary that the expander is in, then you can access the ToggleButton and set the TabIndex in the Expander.Loaded event, like this:

<Expander x:Name="uiExpander"
          Header="_abc"
          Loaded="uiExpander_Loaded"
          TabIndex="20"
          IsTabStop="False">
    <TextBox TabIndex="30">

    </TextBox>
</Expander>


private void uiExpander_Loaded(object sender, RoutedEventArgs e)
{
    //Gets the HeaderSite part of the default ControlTemplate for an Expander.
    var header = uiExpander.Template.FindName("HeaderSite", uiExpander) as Control;
    if (header != null)
    {
        header.TabIndex = uiExpander.TabIndex;
    }
}

You can also just cast the sender object to an Expander too, if you need it to work with multiple expanders. The other option, is to create your own ControlTemplate for the Expander(s) and set it up in there.

EDIT We can also move the code portion to an AttachedProperty, making it much cleaner and easier to use:

<Expander local:ExpanderHelper.HeaderTabIndex="20">
    ...
</Expander>

And the AttachedProperty:

public class ExpanderHelper
{
    public static int GetHeaderTabIndex(DependencyObject obj)
    {
        return (int)obj.GetValue(HeaderTabIndexProperty);
    }

    public static void SetHeaderTabIndex(DependencyObject obj, int value)
    {
        obj.SetValue(HeaderTabIndexProperty, value);
    }

    // Using a DependencyProperty as the backing store for HeaderTabIndex.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty HeaderTabIndexProperty =
        DependencyProperty.RegisterAttached(
        "HeaderTabIndex",
        typeof(int),
        typeof(ExpanderHelper),
        new FrameworkPropertyMetadata(
            int.MaxValue,
            FrameworkPropertyMetadataOptions.None,
            new PropertyChangedCallback(OnHeaderTabIndexChanged)));

    private static void OnHeaderTabIndexChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var expander = o as Expander;
        int index;

        if (expander != null && int.TryParse(e.NewValue.ToString(), out index))
        {
            if (expander.IsLoaded)
            {
                SetTabIndex(expander, (int)e.NewValue);
            }
            else
            {
                // If the Expander is not yet loaded, then the Header will not be costructed
                // To avoid getting a null refrence to the HeaderSite control part we
                // can delay the setting of the HeaderTabIndex untill after the Expander is loaded.
                expander.Loaded += new RoutedEventHandler((i, j) => SetTabIndex(expander, (int)e.NewValue));
            }
        }
        else
        {
            throw new InvalidCastException();
        }
    }

    private static void SetTabIndex(Expander expander, int index)
    {
        //Gets the HeaderSite part of the default ControlTemplate for an Expander.
        var header = expander.Template.FindName("HeaderSite", expander) as Control;
        if (header != null)
        {
            header.TabIndex = index;
        }
    }
}
like image 3
rmoore Avatar answered Sep 19 '22 20:09

rmoore