Preventing a bound TextBox from resetting the caret position

Someone posed a question on our internal mailing list today at work that reminded me of a problem I’d tackled previously whilst working as a developer of fortune.

Here’s the challenge. A TextBox is bound to a data value that is being constantly updated. In my scenario the TextBox was bound to a data feed coming from a serial port connected weigh bridge. Even though the value is being automatically updated the operator has the ability to override the value with their own – at which point it would normally cease being updated by the data service.

Sounds fairly straight-forward. The main problem is that every time the TextBox value is updated via data-binding the selection text and position of the caret is reset. This is particularly annoying if the operator positions the caret about to make their change and a fraction of a second before they press a key the caret moves to the left edge.

I can’t remember exactly how we solved this problem in my earlier engagement (Raaj if you’re listening you could jog my memory) but here was my quick re-attempt.

First I’ll set the scene with a mock environment.

public partial class MainWindow : Window
{
private ViewModel viewModel;
private DispatcherTimer timer;
public MainWindow()
{
InitializeComponent();
this.viewModel = new ViewModel();
this.DataContext = viewModel;
this.timer = new DispatcherTimer(
new TimeSpan(0, 0, 1),
DispatcherPriority.Background,
UpdateValue,
this.Dispatcher); } private void UpdateValue(object sender, EventArgs e) { this.viewModel.Value += 0.01; } }

This sets up a simple form whose DataContext refers to a ViewModel with a Value property. The Value property is updated every second by a thread safe timer.

<Window x:Class="TextBoxOverlay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Width="150">
<TextBox
Text="{Binding Value,StringFormat=0.00,UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</Grid>
</Window>

The XAML simple binds a TextBox to the Value property. Running this sample and the problem can be immediately realised. Attempting to edit the value in the TextBox using the keyboard is extremely frustrating. The caret won’t go where you want it to.

So – the next step is to create a TextBlock that overlays the TextBox and instead bind this to the Value property. We set the IsHitTestVisible property on this TextBlock to False so that the user can still interact with the TextBox underneath. Then – and this is where things get a little sneaky – we make the TextBox’s text transparent. This allows us the strange freedom to interact with the TextBox’s content by selecting it and moving the caret – and because we can see the same text in the overlaid TextBlock things appear as normal.

<Window x:Class="TextBoxOverlay.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Width="150">
<Grid>
<TextBox
Text="{Binding ModifiedValue,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged}"
PreviewTextInput="TextBoxPreviewTextInput" Foreground="Transparent"/> <TextBlock IsHitTestVisible="False" Margin="5,0" Text="{Binding Value,StringFormat=0.00}"/> </Grid> </StackPanel> </Grid> </Window>

You can see from the XAML that the TextBox is bound to a new field on our ViewModel called ModifiedValue. We also hook up to the PreviewTextInput event. We could have used an attached behaviour here rather than resorting to code-behind – but I wanted to keep things simple. So the code behind on the form has:

        private void TextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
{
var textBox = sender as TextBox;
var selectionStart = textBox.SelectionStart;
var selectionLength = textBox.SelectionLength;
var caretIndex = textBox.CaretIndex;
this.viewModel.ModifiedValue = this.viewModel.Value;
textBox.CaretIndex = caretIndex; 
textBox.SelectionStart = selectionStart; textBox.SelectionLength = selectionLength; }

Here we save and restore the TextBox’s SelectionStart, SelectionLength and CaretIndex whilst updating the ModifiedValue that is about to be changed to equal the Value that the user can actually see (remember the ModifiedValue is transparent).

The very last trick is within the ModifiedValue’s setter where we update the Value property. This ensures that whatever changes the operator makes to the TextBox are visible in the overlaid TextBlock. Of course the whole point of doing all of this is that the caret position and selection remains completely unchanged whilst the value appears to update.

        public double? ModifiedValue
{
get
{
return this.modifiedValue;
}
set
{
if (this.modifiedValue != value)
{
this.modifiedValue = value;
NotifyPropertyChanged("ModifiedValue");
if (this.ModifiedValue.HasValue)
Value = ModifiedValue.Value;
}
}
}

Source code here.

So aside from the tacky code-behind to keep the code here to a minimum, I’m wondering if there isn’t a neater solution?

UPDATE: Using an attached behaviour

It was pointed out to me by a colleague that there is a simpler, more versatile solution. Simple encapsulate the text change with selection restore within an attached property. Then we can use multiple bindings to achieve the effect.

        public static string GetNonIntrusiveText(DependencyObject obj)
{
return (string)obj.GetValue(NonIntrusiveTextProperty);
}
public static void SetNonIntrusiveText(DependencyObject obj, string value)
{
obj.SetValue(NonIntrusiveTextProperty, value);
}
public static readonly DependencyProperty NonIntrusiveTextProperty =
DependencyProperty.RegisterAttached(
"NonIntrusiveText",
typeof(string),
typeof(TextBoxExtensions), new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
NonIntrusiveTextChanged)); public static void NonIntrusiveTextChanged(
object sender,
DependencyPropertyChangedEventArgs e) { var textBox = sender as TextBox; if (textBox == null) return; var caretIndex = textBox.CaretIndex; var selectionStart = textBox.SelectionStart; var selectionLength = textBox.SelectionLength; textBox.Text = (string) e.NewValue; textBox.CaretIndex = caretIndex; textBox.SelectionStart = selectionStart; textBox.SelectionLength = selectionLength; }

Now the XAML no longer requires the tricky TextBlock overlay, we simple have a TextBox with two bindings.

<TextBox
Text="{Binding Value,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged,
Mode=OneWayToSource}"
local:TextBoxExtensions.NonIntrusiveText="{Binding Value,StringFormat=0.00,
UpdateSourceTrigger=PropertyChanged,
Mode=TwoWay}"
/>

One thought on “Preventing a bound TextBox from resetting the caret position”

Comments are closed.