3

This is a weird one and I don't really even know what to search for, but trust me I have.

I have a text box and bound to its OnTextChanged event is the below method.

The purpose here is to give the text box focus, move the cursor to the end of the TextBox and return focus back to whatever was actually focused (usually a button). The problem is that it seems the TextBox is not "redrawn" (for lack of a better word?) before I send the focus back to the originally focused element so the cursor position does not update on screen (though all properties think it has).

Currently, I have brutally hacked this together that basically delays the refocus of the previous focused item by 10 ms and runs it in a different thread so the UI has time to update. Now, this is obviously an arbitrary amount of time and works fine on my machine but someone running this app on an older machine may have problems.

Is there a proper way to do this? I can't figure it out.

private void TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e)
{
    if (sender == null) return;
    var box = sender as TextBox;

    if (!box.IsFocused)
    {

        var oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));
        box.Select(box.Text.Length, 0);
        Keyboard.Focus(box); // or box.Focus(); both have the same results

        var thread = new Thread(new ThreadStart(delegate
                                                    {
                                                        Thread.Sleep(10);
                                                        Dispatcher.Invoke(new Action(() => oldFocus.Focus()));
                                                    }));
        thread.Start();
    }
}

EDIT

A new idea I had was to run the oldFocus.Focus() method once the UI is done updating so I tried the following but I get the same result :(

var oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));

Dispatcher.Invoke(DispatcherPriority.Send, new Action(delegate
 {
   box.Select(box.Text.Length, 0);
   box.Focus();
 }));

Dispatcher.Invoke(DispatcherPriority.SystemIdle, new Action(() => oldFocus.Focus()));
Skinner927
  • 953
  • 2
  • 13
  • 25
  • I'm guessing that this is a WPF application, so I've changed the tag. You should usually tell us what kind of application you're writing so that we don't have to guess wrong. – John Saunders Dec 21 '12 at 18:47
  • My apologies. I will not forget in future. – Skinner927 Dec 21 '12 at 18:58
  • Did you try oldFocus.Invalidate() to force a redraw? – Jason Tyler Dec 21 '12 at 19:11
  • 1
    Do you mean caret by "cursor"? – e_ne Dec 21 '12 at 19:13
  • @JasonTyler I have tried box.InvalidateVisual(); with no changes. It seems I can't figure out how to force the UI to redraw the box. Not sure if that's the right terminology. – Skinner927 Dec 21 '12 at 21:46
  • Would you please explain your purpose? Why should you set the caret? – Ramin Dec 22 '12 at 06:47
  • @Ramin I'm trying to right-align the text in a textbox that is populated by either the user or a button press. The contents of the textbox is a file location. I wish the user to see the end of the file location. The only way I've found this to work is to move the caret to the end of the textbox. – Skinner927 Dec 25 '12 at 22:38
  • @Skinner927 how about if you use a ScrollViewer and set the ScrollBar Position? You use HorizontalOffset method. No need to set the HorizontalScrollBarVisibility to be Visible. – Ramin Dec 26 '12 at 04:13

3 Answers3

2

You're on the right track, the problem is that for your .Focus() call to stick, you need to delay the call to a later time inthe Dispatcher.
Instead of using the DispatcherPriority value of Send (which is the highest), try using the Dispatcher to set the focus at a later DispatcherPriority, such as Input.

Dispatcher.BeginInvoke(DispatcherPriority.Input,
new Action(delegate() { 
    oldFocus.Focus();         // Set Logical Focus
    Keyboard.Focus(oldFocus); // Set Keyboard Focus
 }));

As you can see, I'm also setting the Keyboard Focus.
WPF can have multiple Focus Scopes, and more then one element can have Logical Focus (IsFocused = true). But, only one element can have Keyboard Focus and will receive keyboard input.

Blachshma
  • 17,097
  • 4
  • 58
  • 72
  • Thanks for the response. I believe I'm already doing that in my edit, though. I call the select and focus on the text box on the highest level delegate (Send) then I call the lowest level delegate (Idle) to return the focus. The results are no better. – Skinner927 Dec 25 '12 at 22:54
0

After many days, I was finally able to get it to work. It required the Dispatcher to check if the textbox has both focus AND keyboardfocus and lots of loops.

Here's the code for reference. There's some comments in it but if anyone hits this page looking for an answer, you'll have to read through it yourself. A reminder, this is on text change.

protected void TextBox_ShowEndOfLine(object sender, TextChangedEventArgs e)
    {
        if (sender == null) return;
        var box = sender as TextBox;

        if (!box.IsFocused && box.IsVisible)
        {
            IInputElement oldFocus = FocusManager.GetFocusedElement(FocusManager.GetFocusScope(this));
            box.Focus();
            box.Select(box.Text.Length, 0);
            box.Focus();

            // We wait for keyboard focus and regular focus before returning focus to the button
            var thread = new Thread((ThreadStart)delegate
                                        {
                                            // wait till focused
                                            while (true)
                                            {
                                                var focused = (bool)Dispatcher.Invoke(new Func<bool>(() => box.IsKeyboardFocusWithin && box.IsFocused && box.IsInputMethodEnabled), DispatcherPriority.Send);
                                                if (!focused)
                                                    Thread.Sleep(1);
                                                else
                                                    break;
                                            }

                                            // Focus the old element
                                            Dispatcher.Invoke(new Action(() => oldFocus.Focus()), DispatcherPriority.SystemIdle);
                                        });
            thread.Start();
        }
        else if (!box.IsVisible)
        {
            // If the textbox is not visible, the cursor will not be moved to the end. Wait till it's visible.
            var thread = new Thread((ThreadStart)delegate
                                        {
                                            while (true)
                                            {
                                                Thread.Sleep(10);
                                                if (box.IsVisible)
                                                {
                                                    Dispatcher.Invoke(new Action(delegate
                                                                                     {
                                                                                         box.Focus();
                                                                                         box.Select(box.Text.Length, 0);
                                                                                         box.Focus();

                                                                                     }), DispatcherPriority.ApplicationIdle);
                                                    return;
                                                }
                                            }
                                        });
            thread.Start();
        }
    }
Skinner927
  • 953
  • 2
  • 13
  • 25
0

Finally, I found the "right" solution for this issue (full solution at the bottom):

if (!tb.IsFocused)
{
    tb.Dispatcher.BeginInvoke(new Action(() => 
        tb.ScrollToHorizontalOffset(1000.0)), DispatcherPriority.Input);
}

Actually, you don't want to focus the textbox - this hack was required because TextBox.CaretIndex, TextBox.Select() etc. won't do anything if the TextBox does NOT have the focus. Using one of the Scroll methods instead works without focusing. I don't know what exactly the double offset should be (using excessive value of 1000.0 worked for me). The value behaves like pixels, so make sure it's large enough for your scenario.

Next, you don't want to trigger this behavior when the user edits the value using keyboard input. As a bonus I combined vertical and horizontal scrolling, where a multi-line TextBox scrolls vertically, while a single line TextBox scrolls horizontally. Finally, you may want to reuse this thing as a attached property / behavior. Hope you enjoy this solution:

    /// <summary>The attached dependency property.</summary>
    public static readonly DependencyProperty AutoScrollToEndProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(TextBoxBehavior),
            new UIPropertyMetadata(false, AutoScrollToEndPropertyChanged));

    /// <summary>Gets the value.</summary>
    /// <param name="obj">The object.</param>
    /// <returns>The value.</returns>
    public static bool GetAutoScrollToEnd(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToEndProperty);
    }

    /// <summary>Enables automatic scrolling behavior, unless the <c>TextBox</c> has focus.</summary>
    /// <param name="obj">The object.</param>
    /// <param name="value">The value.</param>
    public static void SetAutoScrollToEnd(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToEndProperty, value);
    }

    private static void AutoScrollToEndPropertyChanged(DependencyObject dependencyObject,
        DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var newValue = (bool)e.NewValue;
        if (textBox == null || (bool)e.OldValue == newValue)
        {
            return;
        }
        if (newValue)
        {
            textBox.TextChanged += AutoScrollToEnd_TextChanged;
        }
        else
        {
            textBox.TextChanged -= AutoScrollToEnd_TextChanged;
        }
    }

    private static void AutoScrollToEnd_TextChanged(object sender, TextChangedEventArgs args)
    {
        var tb = (TextBox)sender;
        if (tb.IsFocused)
        {
            return;
        }
        if (tb.LineCount > 1) // scroll to bottom
        {
            tb.ScrollToEnd();
        }
        else // scroll horizontally (what about FlowDirection ??)
        {
            tb.Dispatcher.BeginInvoke(new Action(() => tb.ScrollToHorizontalOffset(1000.0)), DispatcherPriority.Input);
        }
    }

XAML usage:

        <TextBox b:TextBoxBehavior.AutoScrollToEnd="True"
                 Text="{Binding Filename}"/>

where xmlns:b is the corresponding clr-namespace. Happy coding!

Patrick Stalph
  • 792
  • 1
  • 9
  • 19