25 September, 2006

Tip on Event Handling in C# (Compact Framework)

Learnt something while doing some C# programming for the compact framework. There are times when we have code like this to handle the clicking of a button:

private Button btnDoSomething;

private void InitializeComponent()
{
    this.btnDoSomething = new Button();

    this.btnDoSomething.Click
            += new EventHandler(BtnDoSomething_Click);
}

private void BtnDoSomething_Click(Object sender, EventArgs e)
{
    this.btnDoSomething.Enabled = false;

    DoSomething();

    this.btnDoSomething.Enabled = true;
}

private void DoSomething()
{
    // Do Something Here
}


What we are trying to achieve here is to disable the button while the processing is being done, so that it cannot be clicked on for the time being.

However, one problem with this approach is that, while the button is disabled, if it is clicked on, the event will be queued, and fired when it is enabled again, as if the action is performed twice in quick succession.

Here is why. There is an event queue where such events (of clicking on a button) are queued, and an event handler thread that picks up events from the queue and handles them. While the event handler thread is working on one event, new events are queued, and not picked up by the event handler thread immediately. In the case above, when the "Do Something" button is first clicked, the event handler thread will pick up that event and handle it, which is to call the method BtnDoSomething_Click. While this is being done (typically the method DoSomething may take a bit of time), if the button is clicked again, this new event is queued. When the event handler thread finally gets to handle this queued event, the "Do Something" button will already be enabled again (by the last statement in the BtnDoSomething_Click method) and hence the clicking of the button is handled again accordingly.

To get around this, there is an easy way, and a right way. First the easy way:

private void BtnDoSomething_Click(Object sender, EventArgs e)
{
    this.btnDoSomething.Enabled = false;

    DoSomething();

    Application.DoEvents();

    this.btnDoSomething.Enabled = true;
}


Application.DoEvents will cause the event handler thread to handle all queued events at that point in time. This works because, at that point in time, the "Do Something" button is still disabled. So, forcing the click events to be handled at that point in time, it is as if we are simply clearing the events queue. However, the downside is that Application.DoEvents forces ALL queued events to be handled, which means, e.g., if you have other buttons on the form, queued click events from those buttons will also be handled at that time, and things may get messy (remember that while this is all taking place, we are still inside the BtnDoSomething_Click method).

The right way is to do the processing (which is triggered by the clicking of the "Do Something" button) on a seperate thread, so that the event handler thread is quickly freed up to handle new events (including clicking on the same button again while it is still disabled):

private void BtnDoSomething_Click(Object sender, EventArgs e)
{
    this.btnDoSomething.Enabled = false;

    Thread doSomethingThread
            = new Thread(new ThreadStart(DoSomething));
    doSomethingThread.Start();
}

private void DoSomething()
{
    // Do Something Here

    this.btnDoSomething.Enabled = true;
}


4 comments:

Anonymous said...

The final solution of making use of a thread may clear the event queue, however if a user clicks fast enough, the action will still get executed and this time in parallel. That seems worse because you could now have a race condition.

There is another solution involving manually popping the event messages off the queue that seems more likely to suceed.

Anonymous said...

This doesnt work !!!!

Anonymous said...

This poster needs to have a lesson in Thread management. You must never directly touch UI components from a thread other than the main UI thread. If this functionality is desired then do an invoke on the UI component or create a delegate method. Both of these strategies will post an event which then updates the component in it's UI thread.

Anonymous said...

Guys, this code is obviously simplified.

The call to "this.btnDoSomething.Enabled = true;" from inside the "DoSomething()" method is the only thing that will not work as-is.

This little issue can easily be resolved as demonstrated in:
How to: Make Thread-Safe Calls to Windows Forms Controls.

Thanks @edwin11 for sharing this with us.

A.Brontman