How to invoke a UI method from another thread
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();
});
}
Related videos on Youtube
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, 2021Comments
-
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 onlblValue
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 about 12 yearsIs 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 about 12 yearsDoes 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 about 12 yearsI 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 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 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 about 12 years@SkonJeet ....can you edit the code to show me what you mean?
-
Jodrell about 12 yearsUsing 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 about 12 years@whytheq I've embodied my comments in an answer, but it seems I may have wasted my time.
-
-
whytheq about 12 yearsI 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 about 12 yearsno worries Nicolas - not the first time someones put me in my Newbie place and won't be the last :)
-
whytheq about 12 yearsgreat; the use of
DateTime
makes things simpler -
whytheq about 12 yearsputting 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 about 12 yearswhy I used
ElapsedCounter += 1;
rather thanelapesedCounter++;
!! -
whytheq about 12 yearsnot sure why you explicitly made most of the variables
private
...is this not the default? Also why isstartTime
notstatic
? -
Jodrell about 12 years@whytheq, the default scope of fields is private, I was just being explicit, the choice is moot. The
startTime
is notstatic
because it should be set for each instance, not once for all instances. -
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 about 12 yearsas 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 about 12 years@whytheq
TimeSpan
, msdn.microsoft.com/en-us/library/system.timespan.aspx -
whytheq about 12 yearsI've edited the code in my post below to reflect use of Timespan
-
Jodrell about 12 yearsTwo 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, theTimer
class implementsIDisposable
so you should be disposing of the timer stackoverflow.com/questions/475763/… I'll leave further comments for another question -
whytheq about 12 yearsahhh - 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 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 hitselse {myTimer.Stop();}
? -
Jodrell about 12 yearsThe 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
inheritsScrollableControl
which implementsIDisposable
already so, I guess you should extend the base implementation ofIDisposable
to explicitly call dispose onmyTimer
.