How to delay a C# Action like QTimer::singleShot?

Viewed 673

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...*/ );
3 Answers

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.

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();

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();.

Related