简体   繁体   中英

C# - WPF - Prevent an update of a bound focused TextBox

I have a TextBox in a Windows Desktop WPF application bound to a property of a ViewModel. Now the user focuses the TextBox and starts entering a new value. During this time a background process gets a new Value for the same Property (eg because another user in a multi user environment enters a new value and an observer is detecting and propagating this change) and calls a PropertyChanged event for this Property. Now the value changes and the stuff the current user just entered is lost.

Is there a built in way to prevent the change while the TextBox is focused? Or do I have to build my own solution?

I think a custom control is needed to achieve the behavior you describe. By overriding a couple methods on the default WPF TextBox, we can keep the user input even if the View Model changes.

The OnTextChanged method will be called regardless of how our textbox is updated (both for keyboard events and View Model changes), but overriding the OnPreviewKeyDown method will separate out direct user-input. However, OnPreviewKeyDown does not provide easy access to the textbox value because it is also called for non-printable control characters (arrow keys, backspace, etc.)

Below, I made a WPF control that inherits from TextBox and overrides the OnPreviewKeyDown method to capture the exact time of the last user key-press. OnTextChanged checks the time and updates the text only if both events happen in quick succession.

If the last keyboard event was more than a few milliseconds ago, then the update probably did not happen from our user.

public class StickyTextBox : TextBox
{
    private string _lastText = string.Empty;
    private long _ticksAtLastKeyDown;

    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        _ticksAtLastKeyDown = DateTime.Now.Ticks;
        base.OnPreviewKeyDown(e);
    }

    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        if (!IsInitialized)
            _lastText = Text;

        if (IsFocused)
        {
            var elapsed = new TimeSpan(DateTime.Now.Ticks - _ticksAtLastKeyDown);

            // If the time between the last keydown event and our text change is
            // very short, we can be fairly certain than text change was caused
            // by the user.  We update the _lastText to store their new user input
            if (elapsed.TotalMilliseconds <= 5) {
                _lastText = Text;
            }
            else {
                // if our last keydown event was more than a few seconds ago,
                // it was probably an external change
                Text = _lastText;
                e.Handled = true;
            }
        }
        base.OnTextChanged(e);
    }
}

Here's a sample View Model which I used for testing. It updates its own property 5 times from a separate thread every 10 seconds to simulate a background update from another user.

class ViewModelMain : ViewModelBase, INotifyPropertyChanged
{
    private delegate void UpdateText(ViewModelMain vm);
    private string _textProperty;

    public string TextProperty
    {
        get { return _textProperty; }
        set
        {
            if (_textProperty != value)
            {
                _textProperty = value;
                RaisePropertyChanged("TextProperty");
            }
        }
    }

    public ViewModelMain()
    {
        TextProperty = "Type here";

        for (int i = 1; i <= 5; i++)
        {
            var sleep = 10000 * i;

            var copy = i;
            var updateTextDelegate = new UpdateText(vm =>
                vm.TextProperty = string.Format("New Value #{0}", copy));
            new System.Threading.Thread(() =>
            {
                System.Threading.Thread.Sleep(sleep);
                updateTextDelegate.Invoke(this);
            }).Start();
        }
    }
}

This XAML creates our custom StickyTextBox and a regular TextBox bound to the same property to demonstrate the difference in behavior:

<StackPanel>
  <TextBox Text="{Binding TextProperty, UpdateSourceTrigger=PropertyChanged}" Margin="5"/>
  <TextBlock FontWeight="Bold" Margin="5" Text="The 'sticky' text box">
    <local:StickyTextBox Text="{Binding TextProperty}" MinWidth="200" />
  </TextBlock>
</StackPanel>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM