views:

322

answers:

2

In my WPF application, I have a databound TextBox and a databound ItemsControl. The contents of the ItemsControl is determined by the contents of the TextBox. I want to be able to type a value into the TextBox, press tab and enter the first item in the ItemsControl (created from the value in the TextBox ). The problem I am having is that the tab action is evaluated before WPF creates the templated items in the ItemsControl. The following code demonstrates this problem:

<Window x:Class="BindingExample.Window1" x:Name="SelfControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:loc="clr-namespace:BindingExample" Title="Window1" Height="300" Width="400">
    <Window.Resources>
        <DataTemplate DataType="{x:Type loc:ChildClass}">
            <TextBox Text="{Binding Value}" />
        </DataTemplate>
    </Window.Resources>
    <StackPanel DataContext="{Binding ElementName=SelfControl}" Focusable="False">
        <Label Content="Options: A, B, C" />
        <TextBox Text="{Binding Object.Value}" />
        <ItemsControl Margin="16,0,0,0" ItemsSource="{Binding Object.Children}" Focusable="False">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
        <TextBox Text="Box2" />
    </StackPanel>
</Window>


using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace BindingExample
{
    public partial class Window1
    {
        public static readonly DependencyProperty ObjectProperty = DependencyProperty.Register("Object", typeof(ParentClass), typeof(Window1));
        public ParentClass Object
        {
            get { return GetValue(ObjectProperty) as ParentClass; }
            set { SetValue(ObjectProperty, value); }
        }

        public Window1()
        {
            InitializeComponent();
            Object = new ParentClass();
        }
    }

    public class ParentClass : INotifyPropertyChanged
    {
        private string value;
        public string Value 
        {
            get { return value; }
            set { this.value = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Children")); }
        }

        public IEnumerable<ChildClass> Children
        {
            get
            {
                switch (Value.ToUpper())
                {
                    case "A": return new ChildClass[] { "A-1", "A-2", "A-2" };
                    case "B": return new ChildClass[] { "B-1", "B-2", "B-3" };
                    case "C": return new ChildClass[] { "C-1", "C-2", "C-2" };
                    default: return new ChildClass[] { "Unknown" };
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public class ChildClass
    {
        public string Value { get; set; }
        public static implicit operator ChildClass(string value) { return new ChildClass { Value = value }; }
    }
}

In this code, I would like to type "A" into the first TextBox, press tab, and have the focus shift to the child TextBox with the text "A-1". Instead the focus skips to the TextBox with the text "Box2". How can I achieve the behavior I am looking for here?

Note: As Julien Poulin pointed out, it is possible to make this work by switching the UpdateSourceTrigger on the TextBox to PropertyChanged. This only works, however, if "as you type" binding is acceptable. In my case, I would also like to do the set the value and tab with one keystroke. Is there some way to force the ItemsControl to create its templated items on-demand?

+1  A: 

Try to set the UpdateMode of the TextBox to PropertyChanged so the underlying value is set when you type a new value instead of when the TextBox loses focus:

<TextBox Text="{Binding Path=Object.Value, UpdateMode=PropertyChanged}" />
Julien Poulin
This works. Ideally, however, I'd like to have the binding occur when the control loses focus, not while the user types.
Joseph Sturtevant
I don't think it will be possible to automatically set the focus on the TextBox inside the ItemsControl because they are not yet created when your 1st TextBox loses focus. You could write some code to put the focus back where it belongs but it looks like it's going to be really messy.
Julien Poulin
A: 

Here is an alternative solution. It's a bit of a hack, but appears to work. Since the templated objects in the ItemsControl aren't created until execution on the main thread pauses, this code catches the tab, updates the binding, and sets a timer to move the focus once the items have a chance to be created.

...
<TextBox Text="{Binding Object.Value}" KeyDown="TextBox_KeyDown" />
...


public partial class Window1
{
    private DispatcherTimer timer;

    ...

    private void TextBox_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Tab && e.KeyboardDevice.Modifiers != ModifierKeys.Shift)
        {
            e.Handled = true;
            var textbox = (TextBox)sender;
            textbox.GetBindingExpression(TextBox.TextProperty).UpdateSource();
            (timer = new DispatcherTimer(
                new TimeSpan(100000), // 10 ms
                DispatcherPriority.Normal,
                delegate
                {
                    textbox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
                    timer.Stop();
                }, Dispatcher)).Start();
        }
    }
}

The only problem I see with this code is that the ItemsControl might take longer than 10 ms to populate, in which case Tab would jump over those items. Is there any way of detecting whether or not the creation of ItemsControl items has occurred?

Joseph Sturtevant