连续输入时不要提升TextChanged

我有一个文本框,有一个相当沉重的_TextChanged事件处理程序。 在正常的打字条件下,性能还可以,但是当用户执行长时间的连续动作时,它会明显滞后,例如按下退格按钮一次删除大量文本。

例如,事件花费0.2秒完成,但用户每0.1秒执行一次删除。 因此,它无法赶上,并且会有积压的事件需要处理,导致UI滞后。

但是,事件不需要为这些中间状态运行,因为它只关心最终结果。 有没有办法让事件处理程序知道它应该只处理最新的事件,并忽略所有以前陈旧的变化?

根据经验发现这个解决方案简单而且整洁到目前为止,我已经多次遇到过这个问题。 它在Windows Form工作,但可以很容易地转换为WPF

这个怎么运作:

TypeAssistant一个对象被告知发生了text change ,它将运行一个计时器。 然后在WaitingMilliSeconds之后,计时器将引发Idle事件。 通过处理此事件,您可以完成工作。 如果在此期间发生另一个text change ,则计时器将重置。

 public class TypeAssistant { public event EventHandler Idled = delegate { }; public int WaitingMilliSeconds { get; set; } System.Threading.Timer waitingTimer; public TypeAssistant(int waitingMilliSeconds = 600) { WaitingMilliSeconds = waitingMilliSeconds; waitingTimer = new Timer(p => { Idled(this, EventArgs.Empty); }); } public void TextChanged() { waitingTimer.Change(WaitingMilliSeconds, System.Threading.Timeout.Infinite); } } 

用法:

 public partial class Form1 : Form { TypeAssistant assistant; public Form1() { InitializeComponent(); assitant = new TypeAssistant(); assitant.Idled += assitant_Idled; } void assistant_Idled(object sender, EventArgs e) { this.Invoke( new MethodInvoker(() => { // do your job here })); } private void yourFastReactingTextBox_TextChanged(object sender, EventArgs e) { assistant.TextChanged(); } } 

好处:

  • 简单!
  • WPFWindows Form
  • 使用.Net Framework 3.5+

缺点:

  • 再运行一个线程
  • 需要调用而不是直接操纵表单

我也认为Reactive Extensions是去这里的方式。 我的查询略有不同。

我的代码如下所示:

  IDisposable subscription = Observable .FromEventPattern( h => textBox1.TextChanged += h, h => textBox1.TextChanged -= h) .Select(x => textBox1.Text) .Throttle(TimeSpan.FromMilliseconds(300)) .Select(x => Observable.Start(() => /* Do processing */)) .Switch() .ObserveOn(this) .Subscribe(x => textBox2.Text = x); 

现在这正是你期待的方式。

FromEventPatternTextChanged转换为一个返回sender和event args的observable。 Select然后将它们更改为TextBox的实际文本。 如果在300毫秒内发生新的击键,则Throttle基本上忽略了之前的击键 – 因此只传递在滚动的300毫秒窗口内按下的最后一次击键。 Select然后调用处理。

现在,这是魔术。 Switch做了一些特别的事情。 由于select返回了一个observable,我们在Switch之前有一个IObservable>Switch只接受最新生成的observable并从中生成值。 这非常重要。 这意味着如果用户在现有处理运行时键入击键,它将在结束时忽略该结果,并且只报告最新运行处理的结果。

最后有一个ObserveOn将执行返回给UI线程,然后是Subscribe实际处理结果 – 在我的例子中更新第二个TextBox上的TextBox

我认为这段代码非常简洁,非常强大。 您可以使用Nuget为“Rx-WinForms”获取Rx。

您可以将事件处理程序标记为async并执行以下操作:

 bool isBusyProcessing = false; private async void textBox1_TextChanged(object sender, EventArgs e) { while (isBusyProcessing) await Task.Delay(50); try { isBusyProcessing = true; await Task.Run(() => { // Do your intensive work in a Task so your UI doesn't hang }); } finally { isBusyProcessing = false; } } 

尝试使用try-finally子句是为了确保isBusyProcessing在某些时候被保证设置为false ,这样你就不会在无限循环中结束。

一种简单的方法是在内部方法或委托上使用async / await:

 private async void textBox1_TextChanged(object sender, EventArgs e) { // this inner method checks if user is still typing async Task UserKeepsTyping() { string txt = textBox1.Text; // remember text await Task.Delay(500); // wait some return txt != textBox1.Text; // return that text chaged or not } if (await UserKeepsTyping()) return; // user is done typing, do your stuff } 

此处不涉及线程。 对于早于7.0的C#版本,您可以声明委托:

 Func> UserKeepsTyping = async delegate () {...} 

请注意,此方法不会使您偶尔处理相同的“结束重新连接”两次。 例如,当用户键入“ab”,然后立即删除“b”时,您可能最终处理“a”两次。 但这些场合应该是罕见的。 为了避免它们,代码可能是这样的:

 // last processed text string lastProcessed; private async void textBox1_TextChanged(object sender, EventArgs e) { // clear last processed text if user deleted all text if (string.IsNullOrEmpty(textBox1.Text)) lastProcessed = null; // this inner method checks if user is still typing async Task UserKeepsTyping() { string txt = textBox1.Text; // remember text await Task.Delay(500); // wait some return txt != textBox1.Text; // return that text chaged or not } if (await UserKeepsTyping() || textBox1.Text == lastProcessed) return; // save the text you process, and do your stuff lastProcessed = textBox1.Text; } 

Reactive Extensions非常适合处理这种情况。

因此,您希望通过将其限制0.1秒来捕获TextChanged事件并处理输入。 您可以将TextChanged事件转换为IObservable并订阅它。

像这样的东西

 (from evt in Observable.FromEventPattern(textBox1, "TextChanged") select ((TextBox)evt.Sender).Text) .Throttle(TimeSpan.FromMilliSeconds(90)) .DistinctUntilChanged() .Subscribe(result => // process input); 

因此,这段代码订阅TextChanged事件,限制它,确保只获得不同的值,然后从事件args中提取Text值。

请注意,此代码更像是伪代码,我没有测试它。 要使用Rx Linq ,您需要安装Rx-Linq Nuget包 。

如果您喜欢这种方法,可以查看这篇实现使用Rx Linq自动完成控制的博客文章 。 我还建议Bart Re Smet在Reactive Extensions上的精彩演讲 。

使用TextChanged与焦点检查和TextLeave的组合。

 private void txt_TextChanged(object sender, EventArgs e) { if (!((TextBox)sender).Focused) DoWork(); } private void txt_Leave(object sender, EventArgs e) { DoWork(); } 

我不知道如何剔除事件队列,但我可以想到两种方法可以解决这个问题。

如果你想要一些快速的东西(并且根据某些人的标准略微变脏),你可以引入一个排序的等待计时器 – 当validation函数运行时,用当前时间设置一个标志(函数内的静态变量应该足够)。 如果在上次运行和完成的0.5秒内再次调用该函数,则立即退出该函数(大大减少函数的运行时间)。 这将解决积压的事件,前提是它是导致它缓慢而不是触发事件本身的函数的内容。 这样做的缺点是你必须引入某种备份检查以确保当前状态已经过validation – 即,如果最后一次更改发生在0.5s块发生时。

或者,如果您唯一的问题是您不希望在用户进行连续操作时进行validation,则可以尝试修改事件处理程序,以便在按键正在进行时退出而不执行validation,或者甚至可能将validation操作绑定到KeyUp而不是TextChanged。

有很多方法可以实现这一目标。 例如,如果对特定键执行KeyDown事件(例如,对于您的示例执行退格键,但理论上您应该将其扩展为将键入字符的任何内容),validation函数将在不执行任何操作的情况下退出,直到KeyUp事件为止。同一把钥匙被解雇了。 这样它就不会运行,直到最后一次修改……希望如此。

这可能不是达到预期效果的最佳方式(它可能根本不起作用!在用户完成按键操作之前,_TextChanged事件可能会触发),但理论是合理的。 没有花一些时间玩游戏我不能完全确定按键的行为 – 你可以检查按键是否被按下并退出,或者你是否必须手动举起一个标志,这在KeyDown和KeyUp之间是正确的? 一点点玩你的选择应该很清楚你的特定情况的最佳方法是什么。

我希望有所帮助!

你不能沿着以下几行做点什么吗?

 Stopwatch stopWatch; TextBoxEnterHandler(...) { stopwatch.ReStart(); } TextBoxExitHandler(...) { stopwatch.Stop(); } TextChangedHandler(...) { if (stopWatch.ElapsedMiliseconds < threshHold) { stopwatch.Restart(); return; } { //Update code } stopwatch.ReStart() } 
  private async Task ValidateText() { if (m_isBusyProcessing) return; // Don't validate on each keychange m_isBusyProcessing = true; await Task.Delay(200); m_isBusyProcessing = false; // Do your work here. }