Thursday, July 24, 2008

Timers are a Changin' (Part 1)

Sometimes mundane things turn out to be really nice after all. No, I'm not talking about one of your last dates. I'm talking about timers, yes, the Timer classes in C#. These little tools are found everywhere in programming, and it seemed for the longest time, there was only one stock timer to choose from. Well, now there are three.

Before I go any further, let me recommend to you Alex Calvo's MSDN magazine article Timers: Comparing the Timer Classes in the .NET Framework Class Library. Here, you will find some good background for applying timers to your application.

But, in this little series, I want to look at how the timers interact with the event loop. This first article in the series will look at the System.Windows.Form.Timer.

The System.Windows.Form.Timer is supposed to be synchronous with respect to the rest of your Windows Forms app. That means, if you "sleep" or block in some other way, the timer will stop working (while sleeping). However, programmers forget the one caveat. That is, the timer stops working unless "DoEvents" is called (or it's equivalents, let me add). Take a look a the following example. Create a new Forms project. From the toolbox, drop a timer, a button, and a label on the form. Replace the code inside your namespace with the code below. Add a "using System.IO;" to the top. Then link the click and timer events to the appropriate functions below.


public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
timer1.Start();
}

private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog f = new FolderBrowserDialog();
f.ShowDialog(this);
}

private void timer1_Tick(object sender, EventArgs e)
{
label1.Text = DateTime.Now.ToString("hh:mm:ss");
}
}


When you run the example, the time is displayed and updated ever 1/10 of a second (the default 100ms tick). And when you click the button, a FolderBrowserDialog is launched, and the time keeps ticking? Doesn't the article we just read say the timer is synchronous with the rest of our Windows Form app. It says it won't preempt our code as long as we don't call DoEvents(). Yet the modal FolderBrowserDialog has blocked all our other events from being processed. What's going on?

When ever things don't behave as you expect, you have an opportunity to learn something. So, I made a few changes to the code above so that I can analyse further. I added a button to launch a modal MessageBox and a button to call System.Threading.Thread.Sleep(). Now I have...


public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
timer1.Start();
}

private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog f = new FolderBrowserDialog();
f.ShowDialog(this);
}

private void timer1_Tick(object sender, EventArgs e)
{
label1.Text = DateTime.Now.ToString("hh:mm:ss");
}

private void button2_Click(object sender, EventArgs e)
{
MessageBox.Show("hello");
}

private void button3_Click(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(5000);
}
}


Now, when you click on button2, a MessageBox is displayed, and the timer keeps ticking. But if you click on button3, the timer pauses for 5 seconds. So, some operations will stop the timer and some won't. Sleep() will suspend the timer events. But, FolderBrowserDialog and MessageBox and other file dialogs will not. What operations will suspend the timer events? And which operations are modal but will not interfere with the timer? It's important to know because the operations that interfer with the event loop (like Sleep) will interfer with more than just the timer, they will make your application appear to hang.

To answer this question, run the code again, but while its running with no buttons having been clicked, insert a breakpoint in the timer1_tick callback. When the code breaks at this breakpoint, look at the call stack. You should see some external code, above that the main appliction, above that some more external code, and above that the timer1_tick at the top of the stack. Right click on the external code stack element and select the "show external code" context menu item. Reading the stack you can see that above Main() we have the Application.Run() call and some additional calls. These calls implement the main event loop. A timer tick triggers the callback and you find the debuggable callback and further calls to the timer1_tick callback.

Clear the break point and continue your program. Now click button one so that the FolderBrowserDialog opens. Next, set your breakpoint in timer1_Tick() again. Looking at the stack trace this time, we see external code, above that Main(), above that external code, above that button1_Click(), above that somemore external code, and finally above that timer1_Tick(). We deduce a couple important things based upon this experiment. 1) All the controls, including the timer and the FolderBrowserDialog, run in the same thread. That is why sleeping your thread makes everything stop responding. 2) Modal windows do not block the thread (nor do they call DoEvents), they are windows forms in and of their own right and are not required to know or care that they have a parent window. In fact, if you add another Form to the project (say Form2.cs), place a button on it to close the form, and add a button to Form1 to create and show the form using ShowDialog(), then you will see the same behavior.

But, why then are the timer events getting through to Form1 at all while mouse events and other events are not? If the call stack shows the timer1_Tick() getting called deep in the call stack well away from the parent's event loop. The answer lies in the fact that ShowDialog has its own internal event loop. This event loop receives the timer_Tick event, but since it does not belong to this loop, its dispatcher processes the event on the parent window. Some events are eaten by the dispatcher in order to implement "modality", others are not.

The following has button4 added to show the new form. You would have to add a button to your Form1 and link in the button4_Click() handler. And you would need to implement a simple Form2.cs, mine just has an exit button.


public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
timer1.Start();
}

private void button1_Click(object sender, EventArgs e)
{
FolderBrowserDialog f = new FolderBrowserDialog();
f.ShowDialog(this);
}

private void timer1_Tick(object sender, EventArgs e)
{
label1.Text = DateTime.Now.ToString("hh:mm:ss");
}

private void button2_Click(object sender, EventArgs e)
{
MessageBox.Show(this, "hello");
}

private void button3_Click(object sender, EventArgs e)
{
System.Threading.Thread.Sleep(5000);
}

private void button4_Click(object sender, EventArgs e)
{
Form2 frm = new Form2();
frm.ShowDialog(this);
}
}


We have just shown that opening a form with ShowDialog() puts the forms new event loop on the same thread as the main GUI event loop. Form2 is shown modally, so Form1 stops responding to all events except the timer event. But, what if I start a new thread for the second form and ShowDialog() on the new thread, will the event loop run on the new thread separate from the main GUI thread? Yes, but this is well off topic, maybe I'll discuss that in another article.

Well we learned almost nothing about configuring a timer control. But, you should already know how to do that anyway. I simply hope this discussion gives you a little more insight into how the stock System.Windows.Form.Timer will operate under different situations.

In my next article in this series, I will look at the System.Timers.Timer in more detail.

No comments: