Adding a delay before processing Textbox events

Typical scenario: You provide the user with a TextBox he can use to type a filter over a big chunk of data that takes a lot of time to process. Most of the time, you would handle the TextChanged event of the TextBox and issue a filter operation over the data source using the character(s) available in the TextBox. Typical problem: The user complains because he has to wait between the key strokes even if he already knows a filter that would allow the search to take place in a matter of a few seconds. Solution: Introduce a delay between the last key stroke and the processing using a timer. Walkthough: Create a new Winforms project with a Form. Add a TextBox (named textBox), a ListBox (named listBox). To mimic a very slow data source, we will create a dummy class:



public class VerySlowDataAccess
{
    private static string[] data = null;
    static VerySlowDataAccess()
    {
        data = new[]
        {
            "AAAAAA1", "AAAAAA2", "AAAAAA3", "AAAAAA4",
            "AAAAAA5", "AAAAAA6", "AAAAAA7", "BBBBBB1",
            "BBBBBB2", "BBBBBB3", "BBBBBB4", "BBBBBB5",
            "BBBBBB6", "BBBBBB7", "BBBBBB8"
        };
    }
    public static string[] GetItems(string filter)
    {
        Thread.Sleep(3000);
        return data.Where(d => d.StartsWith(filter)).ToArray();
    }
}



Now we can build our very simple form to highlight the concept. Go back the form, add a timer attribute using the System.Threading.Timer and not the System.Windows.Forms.Timer (very important).



private System.Threading.Timer timer;
Then create a thread safe method to populate the ListBox based on the textbox value. By thread-safe, I actually UI-thread safe.
public void LoadData() 
{
    if (this.InvokeRequired)
    {
        this.Invoke(new Action(LoadData));
    }
    else
    {
        this.listBox.Items.Clear();
        this.listBox.Items.AddRange(VerySlowDataAccess.GetItems(textBox.Text));
    } 
}
Next step is to initialize the timer in the Form constructor. We will hook it up with our LoadData method and set the timeout to Infinite to deactivate it (for now):
public MainForm()
{
    InitializeComponent();
    timer = new System.Threading.Timer((c) => LoadData(), null, Timeout.Infinite, Timeout.Infinite);
}
Last but not least, as you may expect, we will add the handler for the TextChanged event of the TextBox.

private void textBox_TextChanged(object sender, EventArgs e)
{
    // 250ms is the maximum time between key strokes before the delegate is called (LoadData in that case)
    timer.Change(250, Timeout.Infinite);
}
So, what is happening here? Basically what we do is simply reset the timer every time the text is changed. Then after the 250 ms delay and if no key was pressed, the delegate will be called, thus refreshing the ListBox. This works because we always set the timer's repeat time to Infinite, it will fire only once after a key stroke sequence. The delay is arbitrarily set to 250 ms. Based on my own experience, it is sufficient for a regular user (with Internet-grade typing skills) and it is not so long as to be noticeable. Possible improvements:
  • Prevent the load to be triggered again until it is finished. A thread-safe boolean in the LoadData method can do the job here.
  • Encapsulate all this behavior into a black-box control.

Comments

  1. Salut !
    Tu tombes super bien avec cet article, j'ai précisément ce cas qui se présente, et j'ai gagné un temps fou.
    Un grand merci !

    ReplyDelete
  2. Thanks a lot !!! Save me a lot 'Googling hours'

    ReplyDelete

Post a Comment

Popular posts from this blog

Change the deployment URL of a ClickOnce application

Handling exceptions the right way in WCF (part 2)