How to invoke a UI method from another thread

89,404

Solution 1

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.

Solution 2

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.

Solution 3

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;
            }
        }
    }
}

Solution 4

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();
  });
}
Share:
89,404

Related videos on Youtube

whytheq
Author by

whytheq

Current addictions: DAX / POWERSHELL Time served with: (T-)sql / MDX / VBA / SSRS Would like more time for the following: C# Python Maxim: if you build something idiot-proof, the world will build a better idiot

Updated on July 15, 2021

Comments

  • whytheq
    whytheq almost 3 years

    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(); }
    
        }
      }
    }
    
    • SkonJeet
      SkonJeet about 12 years
      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.
    • Jodrell
      Jodrell about 12 years
      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.
    • Matthew Watson
      Matthew Watson about 12 years
      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.
    • whytheq
      whytheq about 12 years
      @Jodrell - exactly! but I've looked in here and it seems like adding the following line helps myTimer.SynchronizingObject = this;.....should I answer my own question?!
    • Jodrell
      Jodrell about 12 years
      @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 msdn.microsoft.com/en-us/library/…
    • whytheq
      whytheq about 12 years
      @SkonJeet ....can you edit the code to show me what you mean?
    • Jodrell
      Jodrell about 12 years
      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
      Jodrell about 12 years
      @whytheq I've embodied my comments in an answer, but it seems I may have wasted my time.
  • whytheq
    whytheq about 12 years
    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
    whytheq about 12 years
    no worries Nicolas - not the first time someones put me in my Newbie place and won't be the last :)
  • whytheq
    whytheq about 12 years
    great; the use of DateTime makes things simpler
  • whytheq
    whytheq about 12 years
    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
    whytheq about 12 years
    why I used ElapsedCounter += 1; rather than elapesedCounter++; !!
  • whytheq
    whytheq about 12 years
    not sure why you explicitly made most of the variables private...is this not the default? Also why is startTime not static ?
  • Jodrell
    Jodrell about 12 years
    @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.
  • whytheq
    whytheq about 12 years
    ....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
    whytheq about 12 years
    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.
  • Jodrell
    Jodrell about 12 years
  • whytheq
    whytheq about 12 years
    I've edited the code in my post below to reflect use of Timespan
  • Jodrell
    Jodrell about 12 years
    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 stackoverflow.com/questions/475763/… I'll leave further comments for another question
  • whytheq
    whytheq about 12 years
    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
    whytheq about 12 years
    @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();} ?
  • Jodrell
    Jodrell about 12 years
    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.