22

I have a textbox that has a fairly hefty _TextChanged event handler. Under normal typing condition the performance is okay, but it can noticeably lag when the user performs a long continuous action, such as keeping the backspace button pressed to delete a lot of text at once.

For example, the event took 0.2 seconds to complete, but the user is performing one deletion every 0.1 seconds. Thus, it cannot catch up and there will be a backlog of events that needs to be handled, leading to the UI lagging.

However, the event does not need to run for these in-between states, because it only cares about the end result. Is there any way to let the event handler know that it should process only the latest event, and ignore all the previous stale changes?

ananda
  • 357
  • 1
  • 2
  • 13
  • No, the event does need to be processed as the user types. I just want it to skip the stale events if the changes comes faster than the system can process it. – ananda Nov 18 '15 at 09:45
  • May be this could help you: http://stackoverflow.com/questions/8001450/c-sharp-wait-for-user-to-finish-typing-in-a-text-box – Shaharyar Nov 18 '15 at 09:51

14 Answers14

26

I've come across this problem several times, and based on my own experience I found this solution simple and neat so far. It is based on Windows Form but can be converted to WPF easily.

How it works:

When TypeAssistant learns that a text change has happened, it runs a timer. After WaitingMilliSeconds the timer raises Idle event. By handling this event, you can do whatever job you wish (such as processing the entered tex). If another text change occurs in the time frame starting from the time that the timer starts and WaitingMilliSeconds later, the timer resets.

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);
    }
}

Usage:

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();
    }
}

Advantages:

  • Simple!
  • Working in WPF and Windows Form
  • Working with .Net Framework 3.5+

Disadvantages:

  • Runs one more thread
  • Needs Invocation instead of direct manipulation of form
Alireza
  • 10,237
  • 6
  • 43
  • 59
  • That seems to have fixed all the errors except the one I'm having problem with. The error is at the line `waitingTimer = new Timer(p =>`, specifically at `p`. Thanks for your help. – ananda Nov 19 '15 at 05:41
  • What is the error? Did you include `using System.Threading;` ? – Alireza Nov 19 '15 at 05:42
  • Ah I see, it was automatically referencing System.Windows.Forms.Timer instead. Let me try again. – ananda Nov 19 '15 at 05:44
  • It works, thanks. There are still a few typo in the example (too many s in asssitant. But apparently the system won't allow me to edit when there are only 3 character changes? On another note, I'm using 50ms as the waiting time. Thus, during normal typing the system is still processing the changes very responsively. It is only when a button is continuously held that the change is noticed. – ananda Nov 19 '15 at 06:18
  • @ananda according to my very personal experience, even fast persons cannot type with less than 400ms between keystrokes; so 50ms is a little odd. But it should still work if a key on the keyboard is pressed and remains pressed for long time. I mean after releasing the button, `Idle` event should be fired. – Alireza Nov 19 '15 at 06:28
  • I'm new to WPF. Would you like to explain how this can be converted to WPF? I'm using a Page instead Windows.Forms and Invoke() is not available. – alekstrust Nov 26 '19 at 21:38
  • 1
    @alekstrust Short answer `Dispatcher.Invoke`. See: https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher.invoke?redirectedfrom=MSDN&view=netframework-4.8#remarks – Alireza Nov 27 '19 at 09:16
  • public event EventHandler Idled = delegate { } in VB.net?? – lady Jun 20 '23 at 15:08
18

One easy way is to use async/await on an inner method or delegate:

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    
}

No threading involved here. For C# version older than 7.0, you can declare a delegate:

Func<Task<bool>> UserKeepsTyping = async delegate () {...}

Please note, that this method will not secure you from occasionally processing the same "end reslut" twice. E.g. when user types "ab", and then immediately deletes "b", you might end up processing "a" twice. But these occasions shoud be rare enough. To avoid them, the code could be like this:

// 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;   
}
lisz
  • 435
  • 5
  • 9
13

I also think that the Reactive Extensions are the way to go here. I have a slightly different query though.

My code looks like this:

        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);

Now this works precisely the way you were anticipating.

The FromEventPattern translates the TextChanged into an observable that returns the sender and event args. Select then changes them to the actual text in the TextBox. Throttle basically ignores previous keystrokes if a new one occurs within the 300 milliseconds - so that only the last keystroke pressed within the rolling 300 millisecond window are passed on. The Select then calls the processing.

Now, here's the magic. The Switch does something special. Since the select returned an observable we have, before the Switch, an IObservable<IObservable<string>>. The Switch takes only the latest produced observable and produces the values from it. This is crucially important. It means that if the user types a keystroke while existing processing is running it will ignore that result when it comes and will only ever report the result of the latest run processing.

Finally there's a ObserveOn to return the execution to the UI thread, and then there's the Subscribe to actually handle the result - and in my case update the text on a second TextBox.

I think that this code is incredibly neat and very powerful. You can get Rx by using Nuget for "Rx-WinForms".

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
4

You can mark your event handler as async and do the following:

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 try-finally clause is mandatory to ensure that isBusyProcessing is guaranted to be set to false at some point, so that you don't end up in an infinite loop.

kkyr
  • 3,785
  • 3
  • 29
  • 59
  • I've just tried this, but unfortunately I'm working on .Net 4.0 for this and the async keyword doesn't seem to be supported there. Good to know for other projects, though. Do you have any alternative that will work in this older .Net version? – ananda Nov 18 '15 at 09:59
  • Ah, that's a shame. Unfortunately I don't know any alternatives without async/await. You could take a look at http://blogs.msdn.com/b/bclteam/archive/2012/10/22/using-async-await-without-net-framework-4-5.aspx – kkyr Nov 18 '15 at 10:01
  • Yes, the [`Microsoft.Bcl.Async`](https://www.nuget.org/packages/Microsoft.Bcl.Async/) mentioned in that blog post will work for .NET 4.0. Note: it only works in VS2012 and higher, because the VS2010 and earlier compilers don't recognise the `async` and `await` keywords. –  Nov 18 '15 at 10:25
  • Well if you can't use async/await there's some other answers here that I'm sure can be useful – kkyr Nov 18 '15 at 10:26
  • Elegant solution. Works well on .net 4.5. I'm guessing a background worker could work with older .net versions but it might be a little messy. – Damien Oct 09 '17 at 00:17
  • It looks to me that you might miss the last event. For my situation I can throttle all the events but the last as I'm doing some verification. Maybe you just need to call your work function in the finally. – RJ Thompson Aug 20 '18 at 19:06
2

I played with this for a while. To me this was the most elegant (simple) solution I could come up with:

    string mostRecentText = "";

    async void entry_textChanged(object sender, EventArgs e)
    {
        //get the entered text
        string enteredText = (sender as Entry).Text;

        //set the instance variable for entered text
        mostRecentText = enteredText;

        //wait 1 second in case they keep typing
        await Task.Delay(1000);

        //if they didn't keep typing
        if (enteredText == mostRecentText)
        {
            //do what you were going to do
            doSomething(mostRecentText);
        }
    }
osoblanco
  • 468
  • 2
  • 10
  • This worked best for me...just remember to reset `mostRecentText = "";` after you `doSomething(mostRecentText);` – MX313 Oct 22 '20 at 15:02
  • @MX313 - in my case that's not what I wanted as I leave the entered text on screen. For instance, if they type 'A' and then 3 seconds later hit backspace, if I reset mostRecentText to blank it wouldn't detect the change. – osoblanco Oct 23 '20 at 19:14
2

This is a solution I came up with. It resembles the currently accepted answer, but I find it slightly more elegant, because of two reasons:

  1. It uses an async method that eliminates the need for manual thread marshalling with invoke
  2. There's no need to create a separate event handler.

Lets take a look.

using System;
using System.Threading.Tasks;
using System.Diagnostics;

public static class Debouncer
{
    private static Stopwatch _sw = new Stopwatch();
    private static int _debounceTime;
    private static int _callCount;

    /// <summary>
    ///     The <paramref name="callback"/> action gets called after the debounce delay has expired.
    /// </summary>
    /// <param name="input">this input value is passed to the callback when it's called</param>
    /// <param name="callback">the method to be called when debounce delay elapses</param>
    /// <param name="delay">optionally provide a custom debounce delay</param>
    /// <returns></returns>
    public static async Task DelayProcessing(this string input, Action<string> callback, int delay = 300)
    {
        _debounceTime = delay;

        _callCount++;
        int currentCount = _callCount;

        _sw.Restart();

        while (_sw.ElapsedMilliseconds < _debounceTime) await Task.Delay(10).ConfigureAwait(true);

        if (currentCount == _callCount)
        {
            callback(input);

            // prevent _callCount from overflowing at int.MaxValue
            _callCount = 0;
        }
    }
}

In your form code you can use it as follows:

public partial class Form1 : Form
{

    public Form1()
    {
        InitializeComponent();
    }

    private async void textBox1_TextChanged(object sender, EventArgs e)
    {
        // set the text of label1 to the content of the 
        // calling textbox after a 300 msecs input delay.
        await ((TextBox)sender).Text
            .DelayProcessing(x => label1.Text = x);
    }
}

Note the use of the async keyword on the event handler here. Dont't leave it out.

Explanation

The static Debouncer Class declares an extension method DelayProcessing that extends the string type, so it can be tagged onto the .Text property of a TextBox component. The DelayProcessing method takes a labmda method that get's called as soon as the debounce delay elapses. In the example above I use it to set the text of label control, but you could do all sorts of other things here...

gvdvenis
  • 56
  • 5
1

Reactive Extensions are dealing with this kind of scenarios very nicely.

So you want to capture TextChanged event by throttling it for 0.1 seconds and process the input. You can convert your TextChanged events to IObservable<string> and subscribe to it.

Something like this

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

So this piece of code subscribes to TextChanged events, throttles it, makes sure you get only distinct values and then pulls Text values out of event args.

Please note this code is more like a pseudocode, I didn't test it. In order to use Rx Linq, you will need to install Rx-Linq Nuget package.

If you like this approach, you can check this blog post that implements auto complete control making use of Rx Linq. I would also recommend great talk of Bart De Smet on Reactive Extensions.

Michael
  • 2,961
  • 2
  • 28
  • 54
1

Use a combination of TextChanged with a focus check and TextLeave.

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

private void txt_Leave(object sender, EventArgs e)
{
    DoWork();
}
Shawn Hoover
  • 726
  • 8
  • 9
0

I don't know about culling the event queue, but I can think of two ways you may be able to handle this.

If you want something quick (and slightly dirty by some people's standards), you could introduce a wait timer of sorts - when the validation function runs, set a flag (static variable within the function should suffice) with the current time. if the function is called again within say 0.5 seconds of the last time it ran and completed, exit the function immediately (dramatically reducing the runtime of the function). This will solve the backlog of events, provided it is the contents of the function that is causing it to be slow rather than the firing of the event itself. The downside to this is that you'd have to introduce a backup check of some sort to ensure that the current state has been validated - i.e., if the last change took place while a 0.5s block was happening.

Alternatively, if your only problem is that you don't want the validation to occur when the user is engaging in a continuous action, you could try modifying your event handler such that it exits without performing the validation if a keypress is in progress, or maybe even bind the validation action to KeyUp rather than TextChanged.

There are many ways in which you could achieve this. For example, if a KeyDown event is performed on a particular key (say backspace for your example, but in theory you should extend it to anything that will type a character), the validation function will exit without doing anything until the KeyUp event of the same key is fired. That way it won't run until the last modification has been made... hopefully.

That may not be the most optimal way of achieving the desired effect (it may not work at all! There's a chance that the _TextChanged event will fire before the user has finished pressing the key), but the theory is sound. Without spending some time playing around I can't be absolutely sure on the behaviour of the keypress - can you just check whether the key is pressed and exit, or do you have to raise a flag manually that will be true between KeyDown and KeyUp? A little bit of playing around with your options should make it pretty clear what the best approach will be for your particular case.

I hope that helps!

Ieuan Stanley
  • 1,248
  • 8
  • 20
0

Can't you do something along the following lines?

Stopwatch stopWatch;

TextBoxEnterHandler(...)
{
    stopwatch.ReStart();
}

TextBoxExitHandler(...)
{
    stopwatch.Stop();
}

TextChangedHandler(...)
{
    if (stopWatch.ElapsedMiliseconds < threshHold)
    {
        stopwatch.Restart();
        return;
    }

    {
       //Update code
    }

    stopwatch.ReStart()
}
InBetween
  • 32,319
  • 3
  • 50
  • 90
0
    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.       
    }
RJ Thompson
  • 126
  • 7
0

Jumping off of @lisz's work, it didn't quite work for me in all edge cases but it was close. The UI would fire a false positive sometimes when the user really wasn't finished typing.

Here's the updated code that worked much more smoothly for the user.

private List<Task<bool>> taskTypeQueue = new List<Task<bool>>();
private async void textBox_TextChanged(object sender, EventArgs e)
{
    async Task<bool> isStillTyping()
    {
        Application.DoEvents();

        int taskCount = taskTypeQueue.Count;
        string oldStr = textBox.Text;
        await Task.Delay(1500);

        if ((oldStr != textBox.Text) || (taskCount != taskTypeQueue.Count - 1))
        {
            return true;
        }

        return false;
    }

    taskTypeQueue.Add(isStillTyping());
    if (await taskTypeQueue[taskTypeQueue.Count - 1])
        return;

    // typing appears to have stopped, continue
    taskTypeQueue.Clear();
}
Dan
  • 2,323
  • 1
  • 21
  • 30
0

Here is solution for PowerShell:

  1. Init your stopwatch and timer. Stopwatch will measure total time elapsed since your last character input to the textbox and timer will trigger checks for stopwatch asyncronously every interval (in ms). Hash is your global synchronous hash for your GUI and everything else.
$hash.Stopwatch = New-Object System.Diagnostics.Stopwatch
$hash.Timer = New-Object System.Windows.Forms.Timer
$hash.Timer.Enabled = $true
$hash.Timer.Interval = 100
  1. On every timer tick verify if required time threshold has elapsed.
$hash.Timer.Add_Tick({
    # Write-Host "Elapsed time: $($hash.Stopwatch.Elapsed.Minutes.ToString("00")):$($hash.Stopwatch.Elapsed.Seconds.ToString("00")):$($hash.Stopwatch.Elapsed.Milliseconds.ToString("000"))"
    # Get total time elapsed
    $elapsedMs = $hash.Stopwatch.Elapsed.TotalMilliseconds
    # Set threshold
    $thresholdMs = 1000
    # Check if elapsed time reach threshold
    if ($elapsedMs -ge $thresholdMs) {
        Write-Host "Time has Elapsed. Do Text Validation."
        # Stop stopwatch
        $hash.Stopwatch.Stop()
        # Stop timer
        $hash.Timer.Stop()
        # Check if textbox value is valid
        # .. Your code goes here, for example:
        # Check if textbox value is valid
        $isValid = Test-IfTextboxContentIsValid
        if ($isValid) {
            $hash.YourTextBox.Background = Get-ValidBackgroundColor
        } else {
            $hash.YourTextBox.Background = Get-InvalidBackgroundColor
        }     
})
  1. Add text change handler to your event, which will trigger or reset stopwatch on every character input to the TextBox.
$hash.YourTextBox.Add_TextChanged({
    Write-Host "Text has changed"
    # Reset background color to default
    $this.Background = Get-DefaultBackgroundColor
    # Restart stopwatch (reset time elapsed since previous character is entered)
    $hash.Stopwatch.Restart()
    # Check if timer is not running
    if (-not $hash.Timer.Enabled) {
        # Start timer
        $hash.Timer.Start()
    }
})
  1. Don't forget to dispose timer on application close. Add it to the end of your program code.
$hash.Timer.Dispose()
0

You can create a re-usable user control inheriting TextBox as below:

 public partial class TypeAheadTextBox : TextBox
{
    private bool _typeAheadEnabled = true;
    private int _debounceTimeInMilleconds = 500;
    private readonly Timer _timer;
    private EventArgs _recentTextChangedEventArgs;

    public bool TypeAheadEnabled
    {
        get
        {
            return _typeAheadEnabled;
        }
        set
        {
            _typeAheadEnabled = value;
        }
    }

    public int DebounceTimeInMillSeconds
    {
        get
        {
            return _debounceTimeInMilleconds;
        }
        set
        {
            _debounceTimeInMilleconds = value;
            if(_timer != null)
            {
                _timer.Interval = _debounceTimeInMilleconds;
            }
        }
    }


    public TypeAheadTextBox()
    {
        InitializeComponent();
        _timer = new Timer();
        _timer.Tick += OnTimerTick;
    }

    protected override void OnTextChanged(EventArgs e)
    {
        _recentTextChangedEventArgs = e;

        if (_typeAheadEnabled)
        {
            _timer.Stop();
            _timer.Start();
            return;
        }

        base.OnTextChanged(e);
    }

    private void OnTimerTick(object sender, EventArgs e)
    {
        _timer.Stop();
        base.OnTextChanged(_recentTextChangedEventArgs);
    }
}
Efe
  • 800
  • 10
  • 32