6

Qt has a neat functionality to do timed action with Lambda.

An action can be done after a delay with a single line of code:

    QTimer::singleShot(10, [=](){
        // do some stuff
    });

Although I haven't found equivalent in C#.


The closest I got was

Timer timer = new Timer();
timer.Interval = 10;
timer.Elapsed += (tsender, args) => { 
  // do some stuff 
  timer.Stop();
};
timer.Start();

But it's far from (visually) clean.

Is there a better way to achieve this ?

The use case is sending data on a serial line to some hardware, upon a button click or action, it is often required to send a command, and a packet a few ms later.


Solution with a helper function:

    public void DelayTask(int timeMs, Action lambda)
    {
        System.Timers.Timer timer = new System.Timers.Timer();
        timer.Interval = timeMs;
        timer.Elapsed += (tsender, args) => { lambda.Invoke(); };
        timer.AutoReset = false;
        timer.Start();
    }

Called by

DelayTask(10, () => /* doSomeStuff...*/ );
12
  • 1
    How about you write a function? I doubt QTimer::singleShot looks any cleaner on the inside.
    – tkausl
    Commented Nov 15, 2018 at 11:50
  • @tkausl I agree, I was wondering if there is a stock way that I have missed before writing a function. I'm talking "clean" from a code view prospective.
    – Damien
    Commented Nov 15, 2018 at 11:51
  • 1
    And you don't have to stop the timer if the timer.AutoReset is false (it is default false) Commented Nov 15, 2018 at 11:53
  • 2
    stackoverflow.com/questions/19703740/delay-then-execute-task might be an option
    – tkausl
    Commented Nov 15, 2018 at 11:53
  • @MaximilianAst without the timer.stop I had the line flooded. I guess it's true by default ?
    – Damien
    Commented Nov 15, 2018 at 11:54

3 Answers 3

5

The closest thing I would think of would be something like an helper function like you suggested:

public static class DelayedAction
{
    public static Task RunAsync(TimeSpan delay, Action action)
    {
       return Task.Delay(delay).ContinueWith(t => action(), TaskScheduler.FromCurrentSynchronizationContext());
    }
}

The usage of the class would be close to what you know with Qt:

await DelayedAction.RunAsync(TimeSpan.FromSeconds(10), () => /* do stuff */);

Update

As mentioned in an existing SO question, ContinueWith does not keep the synchronization context by default.

In the current question, the lambda is updating some UI control and, as such, must be run on the UI thread.

To do so, the scheduler must specify the synchronization context when calling the method ContinueWith (TaskScheduler.FromCurrentSynchronizationContext()) to make sure such update is possible.

10
  • async void is a bug waiting to happen, especially in this case. Commented Nov 15, 2018 at 12:55
  • @PanagiotisKanavos Fixed it :)
    – Kzryzstof
    Commented Nov 15, 2018 at 13:17
  • @PanagiotisKanavos there is an issue with the solution (and mine as well), if the function is called more than 1 time and previous actions weren't performed, it overwrites the Lambda.
    – Damien
    Commented Nov 15, 2018 at 13:32
  • @Damien Which solution overwrites the lambda? Both solutions (Panagiotis' and this one) seems to work properly, don't they?
    – Kzryzstof
    Commented Nov 15, 2018 at 13:40
  • You are assigning a Task to a local that is to be thrown away on method exit. Doesn't it strike you as though you might be doing something wrong? Have you thought e.g. about what thread will the lambda run on? Commented Nov 15, 2018 at 13:45
5

You should use System.Threading.Timer instead of System.Timers.Timer. System.Timers.Timer is multithreaded timer meant to be used with desktop applications, which is why it inherits from Component and requires configuration through properties.

With a System.Threading.Timer though you can create a single-fire timer with a single constructor call :

var timer= new Timer(_=>lambda(),null,timeMS,Timeout.Infinite);

This quick & dirty console app:

static void Main(string[] args)
{
    var timeMS = 1000;
    var timer = new Timer(_ => Console.WriteLine("Peekaboo"), null, timeMS, Timeout.Infinite);
    Console.ReadKey();
}

Will print Peekaboo after 1 second even though the main thread is blocked by ReadKey();

2

Using Microsoft's Reactive Framework (NuGet "System.Reactive") you can do this:

IDisposable subscription =
    Observable
        .Timer(TimeSpan.FromMilliseconds(10.0))
        .Subscribe(_ => { /* Do Stuff Here */ });

The IDisposable let's you cancel the subscription before it fires by calling subscription.Dispose();.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.