Reputation: 980
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()));
Upvotes: 3
Views: 6727
Reputation: 860
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!
Upvotes: 0
Reputation: 980
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();
}
}
Upvotes: 0
Reputation: 17385
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.
Upvotes: 2