13

I am trying to delay events in my method by using a timer, however i do not necessarily understand how to use a timer to wait.

I set up my timer to be 2 seconds, but when i run this code the last call runs without a 2 second delay.

Timer timer = new Timer();
timer.Tick += new EventHandler(timer_Tick); // Everytime timer ticks, timer_Tick will be called
timer.Interval = (1000) * (2);              // Timer will tick evert second
timer.Enabled = true;                       // Enable the timer


void timer_Tick(object sender, EventArgs e)
{
    timer.Stop();
}

private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "first";
    timer.Start();
    label1.Text = "second";
}

So when i click my button, it immediately shows label1 as "second", as opposed to changing to "first", waiting 2 seconds, then changing to "second". I have read lots of threads here about using timers instead of thread.sleep, but i cannot seem to find/figure out how to actually implement that.

pbaris
  • 4,525
  • 5
  • 37
  • 61
Fuzz Evans
  • 2,893
  • 11
  • 44
  • 63

4 Answers4

13

If you're using C# 5.0 await makes this much easier:

private async void button1_Click(object sender, EventArgs e)
{
    label1.Text = "first";
    await Task.Delay(2000);
    label1.Text = "second";
}
Servy
  • 202,030
  • 26
  • 332
  • 449
  • Great idea; I think that this is the cleanest approach. However, I think that the "async" keyword needs to precede the "void" keyword. – pennyrave Dec 26 '17 at 15:39
  • Awful idea. Task.Delay in a WinForms event is a recipe for weird behaviours (by stalling the message pump). WinForms is *single-threaded* – smirkingman Nov 09 '19 at 15:48
  • 1
    @smirkingman You can simply run the code yourself to see that, because this is *asynchronous*, the message pump isn't being blocked. Also Winforms isn't "single threaded". You should only be interacting with the UI from one thread, but you can absolutely use additional threads for non-UI work, not that this particular problem requires (nor uses) any additional threads to solve this problem without blocking the UI. – Servy Nov 25 '19 at 23:10
  • @smirkingman only `Thread.Sleep(int);` will stall (and possibly hang) the application's main thread – DitherDude Apr 17 '23 at 10:54
11

timer.Start() just starts the timer but immediately returns while the timer is running in the background. So between setting the label text to first and to second there is nearly no pause. What you want to do is wait for the timer to tick and only then update the label again:

void timer_Tick(object sender, EventArgs e)
{
    timer.Stop();
    label1.Text = "second";
}

private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "first";
    timer.Start();
}

Btw. you should not set timer.Enabled to true, you are already starting the timer using timer.Start().

As mentioned in the comments, you could put the timer creation into a method, like this (note: this is untested):

public void Delayed(int delay, Action action)
{
    Timer timer = new Timer();
    timer.Interval = delay;
    timer.Tick += (s, e) => {
        action();
        timer.Stop();
    };
    timer.Start();
}

And then you could just use it like this:

private void button1_Click(object sender, EventArgs e)
{
    label1.Text = "first";
    Delayed(2000, () => label1.Text = "second");
}

Tergiver’s follow-up

Does using Delayed contain a memory leak (reference leak)?

Subscribing to an event always creates a two-way reference.

In this case timer.Tick gets a reference to an anonymous function (lambda). That function lifts a local variable timer, though it's a reference, not a value, and contains a reference to the passed in Action delegate. That delegate is going to contain a reference to label1, an instance member of the Form. So is there a circular reference from the Timer to the Form?

I don't know the answer, I'm finding it a bit difficult to reason about. Because I don't know, I would remove the use of the lambda in Delayed, making it a proper method and having it, in addition to stopping the timer (which is the sender parameter of the method), also remove the event.

Usually lambdas do not cause problems for the garbage collection. In this case, the timer instance only exists locally and the reference in the lambda does not prevent the garbage collection to collect the instances (see also this question).

I actually tested this again using the .NET Memory Profiler. The timer objects were collected just fine, and no leaking happened. The profiler did give me a warning that there are instances that “[…] have been garbage collected without being properly disposed” though. Removing the event handler in itself (by keeping a reference to it) did not fix that though. Changing the captured timer reference to (Timer)s did not change that either.

What did help—obviously—was to call a timer.Dispose() in the event handler after stopping the timer, but I’d argue if that is actually necessary. I don’t think the profiler warning/note is that critical.

Community
  • 1
  • 1
poke
  • 369,085
  • 72
  • 557
  • 602
  • So if I have numberous places that need to wait, I will end up having a unique timer for each wait correct? – Fuzz Evans Jan 02 '13 at 17:40
  • Yes, you could abstract the boilerplate timer creation into a function though and just call it like `delay(2000, () => label1.Text = "second");`. – poke Jan 02 '13 at 17:44
  • The problem I'm having with this, is the method would otherwise call about 6 events, and each one needs to wait, which causes issues since timer.start() doesn't wait for the timer to execute before continuing, it just starts the timer then proceeds onto the next line. – Fuzz Evans Jan 02 '13 at 18:13
  • Yeah, that’s the point of the timer, so that you can do something after a certain time *without* blocking the thread. See the addition to the answer on an example. – poke Jan 02 '13 at 18:20
  • @poke I wondered about whether or not there is a memory leak in that Delayed code, but it was too long to muse about in a comment. Feel free to delete what I edited into your answer. – Tergiver Jan 02 '13 at 19:13
  • @Tergiver Did some thinking and more importantly testing, and it seems to be gc'd just fine. See my changed answer for further details. – poke Jan 02 '13 at 20:10
  • @poke Thanks for that. I'm still trying to figure out how, if it gets collected, where is the reference that keeps it alive long enough to work? – Tergiver Jan 02 '13 at 20:31
  • @Tergiver I’m not into the internals enough to possibly answer that. Maybe you can create a (real) follow-up question on that topic? – poke Jan 02 '13 at 20:34
  • 2
    Ah, the answer is that when you start a System.Windows.Forms.Timer, it creates (and holds) a System.Windows.Forms.NativeWindow object which adds itself to a lookup table which associates a native window handle with a NativeWindow object. When the timer is destroyed, that object is removed from the map. So there is the reference which keeps it alive while it works. – Tergiver Jan 02 '13 at 22:03
  • 2
    For the sake of accuracy, Timer creates a Timer.TimerNativeWindow object which is a sub-class of NativeWindow that has a back-pointer to the Timer object, thus the keep-alive reference. – Tergiver Jan 02 '13 at 23:05
0

If all you're trying to do is change the text when the timer ticks, would you not be better off putting...

label1.Text = "second";

...In the timer tick, either before or after you change the timer to enabled = false;

Like so;

void timer_Tick(object sender, EventArgs e)
{
  timer.Stop();
  label1.Text = "second";
}

private void button1_Click(object sender, EventArgs e)
{
  label1.Text = "first";
  timer.Start();
}
Kestami
  • 2,045
  • 3
  • 32
  • 47
  • 1
    Using `Sysmtes.Timers.Timer` this code wouldn't work, you'd need to marshal to the UI thread. The advantage of a `Forms` timer is that you don't need to do that. – Servy Jan 02 '13 at 17:12
  • That's very true, forgot about that. I'll either delete that part or add some invokey stuff in there, just don't wanna confuse OP. – Kestami Jan 02 '13 at 17:13
0
       private bool Delay(int millisecond)       
       {

           Stopwatch sw = new Stopwatch();
           sw.Start();
           bool flag = false;
           while (!flag)
           {
               if (sw.ElapsedMilliseconds > millisecond) 
               {
                  flag = true;
               }
           }
           sw.Stop();
           return true;

       }

        bool del = Delay(1000);
Yuval
  • 547
  • 1
  • 7
  • 14