我认为需要一个自定义控件来实现您描述的行为。通过覆盖默认 WPF TextBox 上的几个方法,即使 View Model 发生更改,我们也可以保留用户输入。
无论我们的文本框如何更新(键盘事件和视图模型更改),OnTextChanged 方法都将被调用,但覆盖OnPreviewKeyDown 方法将分离出直接用户输入。但是,OnPreviewKeyDown 不提供对文本框值的轻松访问,因为它也被用于不可打印的控制字符(箭头键、退格等)
下面,我创建了一个 WPF 控件,它继承自 TextBox 并覆盖 OnPreviewKeyDown 方法以捕获最后一次用户按键的确切时间。 OnTextChanged 仅在两个事件快速连续发生时检查时间并更新文本。
如果最后一个键盘事件发生在几毫秒之前,那么更新可能不是来自我们的用户。
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);
}
}
这是我用于测试的示例视图模型。它每 10 秒从一个单独的线程更新自己的属性 5 次,以模拟另一个用户的后台更新。
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();
}
}
}
此 XAML 创建我们的自定义 StickyTextBox 和绑定到同一属性的常规 TextBox 以展示行为差异:
<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>