26

Playing round with Timers. Context: a winforms with two labels.

I would like to see how System.Timers.Timer works so I've not used the Forms timer. I understand that the form and myTimer will now be running in different threads. Is there an easy way to represent the elapsed time on lblValue in the following form?

I've looked here on MSDN but is there an easier way !

Here's the winforms code:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {
    //instance variables of the form
    System.Timers.Timer myTimer;
    int ElapsedCounter = 0;

    int MaxTime = 5000;
    int elapsedTime = 0;
    static int tickLength = 100;

    public AirportParking()
    {
        InitializeComponent();
        keepingTime();
        lblValue.Text = "hello";
    }

    //method for keeping time
    public void keepingTime() {

        myTimer = new System.Timers.Timer(tickLength); 
        myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);

        myTimer.AutoReset = true;
        myTimer.Enabled = true;

        myTimer.Start();
    }


    void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        myTimer.Stop();
        ElapsedCounter += 1;
        elapsedTime += tickLength; 

        if (elapsedTime < MaxTime)
        {
            this.lblElapsedTime.Text = elapsedTime.ToString();

            if (ElapsedCounter % 2 == 0)
                this.lblValue.Text = "hello world";
            else
                this.lblValue.Text = "hello";

            myTimer.Start(); 

        }
        else
        { myTimer.Start(); }

    }
  }
}
Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208
whytheq
  • 34,466
  • 65
  • 172
  • 267
  • Is it necessary to stop the timer every time it ticks past the specified elapsed time? I'd probably conditionally stop it - not stop it and then conditionally start it. – SkonJeet Apr 16 '12 at 08:01
  • Does this code actually work, or does a cross thread exception in the handler get suppressed? I haven't tryed this code but I would have thought that, since the handler can run on any thread from the thread pool, updating the label on the GUI would mostly result in a cross thread exception. – Jodrell Apr 16 '12 at 08:01
  • I think that way of calculating the elapsed time is likely to be fairly imprecise. You would be much better off using a Stopwatch object to actually calculate the elapsed time (but still use a timer to periodically update the control). Of course, this doesn't really matter at all if all you are doing is learning about timers. – Matthew Watson Apr 16 '12 at 08:02
  • @Jodrell - exactly! but I've looked in [here](http://msdn.microsoft.com/en-us/magazine/cc164015.aspx) and it seems like adding the following line helps `myTimer.SynchronizingObject = this;`.....should I answer my own question?! – whytheq Apr 16 '12 at 08:04
  • @whytheq, that would cause the timer to do the cross thread invokes for you, didn't see that in example. Alternatively you do the check yourself with `Control.InvokeRequired` http://msdn.microsoft.com/en-us/library/system.windows.forms.control.invokerequired.aspx – Jodrell Apr 16 '12 at 08:09
  • @SkonJeet ....can you edit the code to show me what you mean? – whytheq Apr 16 '12 at 08:10
  • Using timers to count time like this wont work either. You should use the elapsed event to rwad the time from some more relaible source. – Jodrell Apr 16 '12 at 08:14
  • @whytheq I've embodied my comments in an answer, but it seems I may have wasted my time. – Jodrell Apr 16 '12 at 08:44

5 Answers5

47

I guess your code is just a test so I won't discuss about what you do with your timer. The problem here is how to do something with an user interface control inside your timer callback.

Most of Control's methods and properties can be accessed only from the UI thread (in reality they can be accessed only from the thread where you created them but this is another story). This is because each thread has to have its own message loop (GetMessage() filters out messages by thread) then to do something with a Control you have to dispatch a message from your thread to the main thread. In .NET it is easy because every Control inherits a couple of methods for this purpose: Invoke/BeginInvoke/EndInvoke. To know if executing thread must call those methods you have the property InvokeRequired. Just change your code with this to make it works:

if (elapsedTime < MaxTime)
{
    this.BeginInvoke(new MethodInvoker(delegate 
    {
        this.lblElapsedTime.Text = elapsedTime.ToString();

        if (ElapsedCounter % 2 == 0)
            this.lblValue.Text = "hello world";
        else
            this.lblValue.Text = "hello";
    }));
}

Please check MSDN for the list of methods you can call from any thread, just as reference you can always call Invalidate, BeginInvoke, EndInvoke, Invoke methods and to read InvokeRequired property. In general this is a common usage pattern (assuming this is an object derived from Control):

void DoStuff() {
    // Has been called from a "wrong" thread?
    if (InvokeRequired) {
        // Dispatch to correct thread, use BeginInvoke if you don't need
        // caller thread until operation completes
        Invoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

Note that current thread will block until UI thread completed method execution. This may be an issue if thread's timing is important (do not forget that UI thread may be busy or hung for a little). If you don't need method's return value you may simply replace Invoke with BeginInvoke, for WinForms you don't even need subsequent call to EndInvoke:

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

If you need return value then you have to deal with usual IAsyncResult interface.

How it works?

A GUI Windows application is based on the window procedure with its message loops. If you write an application in plain C you have something like this:

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}

With these few lines of code your application wait for a message and then delivers the message to the window procedure. The window procedure is a big switch/case statement where you check the messages (WM_) you know and you process them somehow (you paint the window for WM_PAINT, you quit your application for WM_QUIT and so on).

Now imagine you have a working thread, how can you call your main thread? Simplest way is using this underlying structure to do the trick. I oversimplify the task but these are the steps:

  • Create a (thread-safe) queue of functions to invoke (some examples here on SO).
  • Post a custom message to the window procedure. If you make this queue a priority queue then you can even decide priority for these calls (for example a progress notification from a working thread may have a lower priority than an alarm notification).
  • In the window procedure (inside your switch/case statement) you understand that message then you can peek the function to call from the queue and to invoke it.

Both WPF and WinForms use this method to deliver (dispatch) a message from a thread to the UI thread. Take a look to this article on MSDN for more details about multiple threads and user interface, WinForms hides a lot of these details and you do not have to take care of them but you may take a look to understand how it works under the hood.

Noah Heber
  • 82
  • 1
  • 12
Adriano Repetti
  • 65,416
  • 20
  • 137
  • 208
18

Personally when I work in an application that works with threads out of the UI one, I usually write this little snippet:

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}

When I do an async call in a different thread I can always callback using:

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});

Simple and clean.

Bernardo Ramos
  • 4,048
  • 30
  • 28
2

As asked, here is my answer that checks for cross thread calls, synchronises variable updates, doesen't stop and start the timer and doesn't use the timer for counting elapsed time.

EDIT fixed BeginInvoke call. I've done the cross thread invoke using a generic Action, This allows the sender and eventargs to be passed. If these are unused (as they are here) it is more efficient to use MethodInvoker but I suspect the handling would need to be moved into a parameterless method.

public partial class AirportParking : Form
{
    private Timer myTimer = new Timer(100);
    private int elapsedCounter = 0;
    private readonly DateTime startTime = DateTime.Now;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    public AirportParking()
    {
        lblValue.Text = EvenText;
        myTimer.Elapsed += MyTimerElapsed;
        myTimer.AutoReset = true;
        myTimer.Enabled = true;
        myTimer.Start();
    }

    private void MyTimerElapsed(object sender,EventArgs myEventArgs)
    {
        If (lblValue.InvokeRequired)
        {
            var self = new Action<object, EventArgs>(MyTimerElapsed);
            this.BeginInvoke(self, new [] {sender, myEventArgs});
            return;   
        }

        lock (this)
        {
            lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
            elapesedCounter++;
            if(elapsedCounter % 2 == 0)
            {
                lblValue.Text = EvenText;
            }
            else
            {
                lblValue.Text = OddText;
            }
        }
    }
}
Jodrell
  • 34,946
  • 5
  • 87
  • 124
  • great; the use of `DateTime` makes things simpler – whytheq Apr 16 '12 at 12:01
  • putting the EvenText / OddText as instance variables seems like better practice, as I should avoid leaving hard-coded strings in the middle of my code – whytheq Apr 16 '12 at 12:03
  • why I used `ElapsedCounter += 1;` rather than `elapesedCounter++;` !! – whytheq Apr 16 '12 at 12:04
  • not sure why you explicitly made most of the variables `private`...is this not the default? Also why is `startTime` not `static` ? – whytheq Apr 16 '12 at 12:05
  • @whytheq, the default scope of fields is private, I was just being explicit, the choice is moot. The `startTime` is not `static` because it should be set for each instance, not once for all instances. – Jodrell Apr 16 '12 at 13:02
  • ....thanks for all the help; your code was bugging out as the `sender` argument of the BeginInvoke method was asking for a delegate ....so I ended up using the below – whytheq Apr 16 '12 at 13:26
  • as an aside if I wanted to put `DateTime.Now.SubTract(startTime)` into a variable what type should it be? Reason I ask is that my code wants to compare this time to a MAXIMUM time and if above that then the Timer should stop. – whytheq Apr 16 '12 at 14:41
  • 1
    @whytheq `TimeSpan`, http://msdn.microsoft.com/en-us/library/system.timespan.aspx – Jodrell Apr 16 '12 at 15:25
  • I've edited the code in my post below to reflect use of Timespan – whytheq Apr 17 '12 at 07:00
1

First, in Windows Forms (and most frameworks), a control can only be accessed (unless documented as "thread safe") by the UI thread.

So this.lblElapsedTime.Text = ... in your callback is plain wrong. Take a look at Control.BeginInvoke.

Second, You should use System.DateTime and System.TimeSpan for your time computations.

Untested:

DateTime startTime = DateTime.Now;

void myTimer_Elapsed(...) {
  TimeSpan elapsed = DateTime.Now - startTime;
  this.lblElapsedTime.BeginInvoke(delegate() {
    this.lblElapsedTime.Text = elapsed.ToString();
  });
}
Nicolas Repiquet
  • 9,097
  • 2
  • 31
  • 53
  • I realised `this.lblElapsed...` was _plain wrong_ when the compiler told me the error about incorrect threading! Thanks for the advice in terms of the correct Tim objects to use much appreciated (+1) – whytheq Apr 16 '12 at 08:51
  • no worries Nicolas - not the first time someones put me in my Newbie place and won't be the last :) – whytheq Apr 16 '12 at 09:38
0

Ended up using the following. It's a combination of the suggestions given:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    //instance variables of the form
    System.Timers.Timer myTimer;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    static int tickLength = 100; 
    static int elapsedCounter;
    private int MaxTime = 5000;
    private TimeSpan elapsedTime; 
    private readonly DateTime startTime = DateTime.Now; 
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    public AirportParking()
    {
        InitializeComponent();
        lblValue.Text = EvenText;
        keepingTime();
    }

    //method for keeping time
    public void keepingTime() {

    using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
    {  
           myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
           myTimer.AutoReset = true;
           myTimer.Enabled = true;
           myTimer.Start(); 
    }  

    }

    private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        elapsedCounter++;
        elapsedTime = DateTime.Now.Subtract(startTime);

        if (elapsedTime.TotalMilliseconds < MaxTime) 
        {
            this.BeginInvoke(new MethodInvoker(delegate
            {
                this.lblElapsedTime.Text = elapsedTime.ToString();

                if (elapsedCounter % 2 == 0)
                    this.lblValue.Text = EvenText;
                else
                    this.lblValue.Text = OddText;
            })); 
        } 
        else {myTimer.Stop();}
      }
  }
}
whytheq
  • 34,466
  • 65
  • 172
  • 267
  • Two last comments, now that you leave the timer running (which is good) you should probabaly have a `else` in the handler to stop the timer. Additionally, the `Timer` class implements `IDisposable` so you should be disposing of the timer http://stackoverflow.com/questions/475763/is-it-necessary-to-dispose-system-timers-timer-if-you-use-one-in-your-applicatio I'll leave further comments for another question – Jodrell Apr 17 '12 at 08:23
  • ahhh - so even when I run the application and it seemed to Stop...it was still running in it's own thread. I'll add the else now. Best in future if I ever use these Timer's to always think about when they stop. – whytheq Apr 17 '12 at 09:05
  • @Jodrell - I've added the 'using' structure in the `KeepingTim()` method...not sure if this will mean the above will trip up when it hits `else {myTimer.Stop();}` ? – whytheq Apr 17 '12 at 21:00
  • The using will work here, whilst it is usually a good option. Once the using block is exited the timer will be immediately disposed so, probably won't ever fire. `Form` inherits `ScrollableControl` which implements `IDisposable` already so, I guess you should extend the base implementation of `IDisposable` to explicitly call dispose on `myTimer`. – Jodrell Apr 18 '12 at 10:00