5

I have an application that start System.Threading.Timer, then this timer every 5 seconds read some information from a linked database and update GUI on main form of application;

Since the System.Threading.Timer create another thread for the Tick event, i need to use Object.Invoke for updating User Interface on the main Form of application with code like this :

this.Invoke((MethodInvoker)delegate()
  {
       label1.Text = "Example";
  });

The app work very well, but sometimes when the user close the main form and then close the application, if the second thread on timer_tick event is updating the user interface on main thread the user get an ObjectDisposedException.

How can i do for stop and close the threading timer before closing the main form and avoiding then Object disposed exception ?

ThiefMaster
  • 310,957
  • 84
  • 592
  • 636
aleroot
  • 71,077
  • 30
  • 176
  • 213
  • `System.Threading.Timer` does not have a `Tick` event. The only BCL timer that actually has the `Tick` event is the `System.Windows.Forms.Timer`. Can you clarify which one you are using? This is important. – Brian Gideon Sep 04 '10 at 20:33

3 Answers3

7

This is a bit of a tricky proposition as you must ensure the following on a given Close event

  1. The timer is stopped. This is fairly straight forward
  2. The control being updated isn't disposed when the delegate is run. Again straight forward.
  3. The code currently running off of a timer tick has completed. This is harder but doable
  4. There are no pending Invoke methods. This is quite a bit harder to accomplish

I've run into this problem before and I've found that preventing this problem is very problematic and involves a lot of messy, hard to maintain code. It's much easier to instead catch the exceptions that can arise from this situation. Typically I do so by wrapping the Invoke method as follows

static void Invoke(ISynchronizedInvoke invoke, MethodInvoker del) {
  try {
    invoke.Invoke(del,null);
  } catch ( ObjectDisposedException ) {
    // Ignore.  Control is disposed cannot update the UI.
  }
}

There is nothing inherently wrong with ignoring this exception if you're comfortable with the consequences. That is if your comfortable with the UI not updating after it's already been disposed. I certainly am :)

The above doesn't take care of issue #2 though and it still needs to be done manually in your delegate. When working with WinForms I often use the following overload to remove that manual check as well.

static void InvokeControlUpdate(Control control, MethodInvoker del) {
  MethodInvoker wrapper = () => {
    if ( !control.IsDisposed ) {
      del();
    }
  };
  try {
    control.Invoke(wrapper,null);
  } catch ( ObjectDisposedException ) {
    // Ignore.  Control is disposed cannot update the UI.
  }
}

Note

As Hans noted ObjectDisposedException is not the only exception that can be raised from the Invoke method. There are several others, including at least InvalidOperationException that you need to consider handling.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • +1: Am I correct in thinking that for the second example, if we assume the only thread that is going to dispose of a UI object is the UI thread (which *should* be the case), you should never see an `ObjectDisposedException`, as the check for `control.IsDisposed` is carried out on the UI thread? – Alex Humphrey Sep 04 '10 at 10:33
  • That's not the only exception that can be raised, you'll get InvalidOperationException as well. – Hans Passant Sep 04 '10 at 16:01
  • @Hans, forgot about that one, added a note. – JaredPar Sep 05 '10 at 02:07
  • I've added your code in my form class, but for the overload of the 1st method i get an error at line : invoke.Invoke(del); because invoke can't take only one argument. – aleroot Sep 06 '10 at 07:34
3

System.Timers.Timer is a horrible class. There is no good way to stop it reliably, there is always a race and you can't avoid it. The problem is that its Elapsed event gets raised from a threadpool thread. You cannot predict when that thread actually starts running. When you call the Stop() method, that thread may well have already been added to the thread pool but didn't get around to running yet. It is subject to both the Windows thread scheduler and the threadpool scheduler.

You can't even reliably solve it by arbitrarily delaying the closing of the window. The threadpool scheduler can delay the running of a thread by up to 125 seconds in the most extreme cases. You'll reduce the likelihood of an exception by delaying the close by a couple of seconds, it won't be zero. Delaying the close for 2 minutes isn't realistic.

Just don't use it. Either use System.Threading.Timer and make it a one-shot timer that you restart in the event handler. Or use a System.Windows.Forms.Timer, it is synchronous.

A WF Timer should be your choice here because you use Control.Invoke(). The delegate target won't start running until your UI thread goes idle. The exact same behavior you'll get from a WF timer.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • What is WF Timer ? is equal to DispatcherTimer ? I'm actually using System.Threading.Timer and not System.Timers.Timer. – aleroot Sep 06 '10 at 11:01
  • Windows Forms Timer. Yes, same thing as the WPF DispatcherTimer. I explained how to use a System.Threading.Timer, set the period to 0. – Hans Passant Sep 06 '10 at 11:55
  • Now i start the System.Threading.Timer thus : System.Threading.TimerCallback oCallbackGrid = new System.Threading.TimerCallback(GridTimer_Tick); timerRefreshGrid = new System.Threading.Timer(oCallbackGrid, null, 1000, 3000); because i need to fire GridTimer_Tick every 3 seconds. How can i do otherwise ? – aleroot Sep 06 '10 at 12:25
  • How can i run the GridTimer_Tick method every 3 seconds in another thread setting period to 0 ??? – aleroot Sep 06 '10 at 12:30
  • Call the Change() method in the event handler to restart the timer. What does the callback do other than calling BeginInvoke? – Hans Passant Sep 06 '10 at 12:35
  • The callback query a database for get row to insert in a datatable, i need to run this callback every 3 seconds. What event handler i need to use for change the period ? Can you make me an example ? – aleroot Sep 06 '10 at 12:46
  • These kind of error (InvalidOperationException and ObjectDisposedException) happen sometimes only when i close the application, and the timer event try to update UI. – aleroot Sep 06 '10 at 12:53
  • But of course, it only bombs when the form is closed. Use this approach: http://stackoverflow.com/questions/1731384/how-to-stop-backgroundworker-on-forms-closing-event/1732361#1732361 – Hans Passant Sep 06 '10 at 13:20
1

Create two booleans called 'StopTimer' and 'TimerStopped'. Set the timer's AutoReset property to false. Then format the Elapsed method to the following:

TimerStopped = false;
Invoke((MethodInvoker)delegate {
    // Work to do here.
});
if (!StopTimer)
    timer.Start();
else
    TimerStopped = true;

This way you are preventing a race condition, checking if the timer should continue and reporting when the method has reached its end.

Now format your FormClosing event as follows:

if (!TimerStopped)
{
    StopTimer = true;
    Thread waiter = new Thread(new ThreadStart(delegate {
        while (!TimerStopped) { }
        Invoke((MethodInvoker)delegate { Close(); });
    }));
    waiter.Start();
    e.Cancel = true;
}
else
    timer.Dispose();

If the timer hasn't stopped yet, a thread is launched to wait until it has done so and then try to close the form again.