连续输入时不要提升TextChanged

ana*_*nda 12 c# .net-4.0 winforms

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

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

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

Ali*_*eza 12

我根据经验发现这个问题简单而且整洁到目前为止已经多次遇到过这个问题.它正在工作Windows Form但很容易转换为WPF.

这个怎么运作:

当一个对象TypeAssistant被告知a 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

public partial class Form1 : Form
{
    TypeAssistant assistant;
    public Form1()
    {
        InitializeComponent();
        assistant = new TypeAssistant();
        assistant.Idled += assistant_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();
    }
}
Run Code Online (Sandbox Code Playgroud)

好处:

  • 简单!
  • WaitingMilliSeconds和中工作WPF
  • 使用.Net Framework 3.5+

缺点:

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


Eni*_*ity 11

我也认为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);
Run Code Online (Sandbox Code Playgroud)

现在这正是你所期待的方式.

将其FromEventPattern转换TextChanged为可返回发送方和事件参数的observable.Select然后将它们更改为中的实际文本TextBox.Throttle如果在300毫秒内发生新的按键,则基本上忽略了之前的击键- 因此只300传递在滚动毫秒窗口内按下的最后一次击键.在Select随后调用的处理.

现在,这是魔术.在Switch做一些特别的东西.由于select返回了一个observable,我们之前有Switch一个IObservable<IObservable<string>>.在Switch只有最新生产的可观察的需要,并从它产生的价值.这非常重要.这意味着如果用户在现有处理运行时键入击键,它将在结束时忽略该结果,并且只会报告最新运行处理的结果.

最后有一个ObserveOn将执行返回到UI线程,然后Subscribe实际处理结果 - 在我的情况下,在一秒钟更新文本TextBox.

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


lis*_*isz 6

一种简单的方法是对内部方法或委托使用异步/等待:

private async void textBox1_TextChanged(object sender, EventArgs e) {
    // this inner method checks if user is still typing
    async Task<bool> 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    
}
Run Code Online (Sandbox Code Playgroud)

这里不涉及线程。对于低于7.0的C#版本,可以声明一个委托:

Func<Task<bool>> UserKeepsTyping = async delegate () {...}
Run Code Online (Sandbox Code Playgroud)

请注意,这种方法不会使您偶尔处理两次相同的“最终结果”。例如,当用户键入“ 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<bool> 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;   
}
Run Code Online (Sandbox Code Playgroud)