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...*/ );
Kzryzstof
  • 7,688
  • 10
  • 61
  • 108
Damien
  • 1,492
  • 10
  • 32
  • 1
    How about you write a function? I doubt `QTimer::singleShot` looks any cleaner on the inside. – tkausl Nov 15 '18 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 Nov 15 '18 at 11:51
  • 1
    And you don't have to stop the timer if the `timer.AutoReset` is false (it is default false) – Maximilian Ast Nov 15 '18 at 11:53
  • 2
    https://stackoverflow.com/questions/19703740/delay-then-execute-task might be an option – tkausl Nov 15 '18 at 11:53
  • @MaximilianAst without the timer.stop I had the line flooded. I guess it's true by default ? – Damien Nov 15 '18 at 11:54
  • @tkausl would that be thread safe? – Damien Nov 15 '18 at 11:55
  • @MaximilianAst with nothing, the timer keeps calling, with timer.AutoReset = false; it stops at the first timeout. – Damien Nov 15 '18 at 12:08
  • It was an error on my side. Yes the default is `true` – Maximilian Ast Nov 15 '18 at 12:13
  • "Solution with a helper function:", question box is for question. If it's an answer use the answer box. :) – Drag and Drop Nov 15 '18 at 12:20
  • @Damien instead of `System.Timers.Timer` you can use a [System.Threading.Timer](https://learn.microsoft.com/en-us/dotnet/api/system.threading.timer.-ctor?view=netframework-4.7.2#System_Threading_Timer__ctor_System_Threading_TimerCallback_System_Object_System_Int32_System_Int32_) and configure it to fire just once in the constructor, eg `new Timer(_=>lambda(),null,timeMS,Timeout.Infinite);` – Panagiotis Kanavos Nov 15 '18 at 13:01
  • @PanagiotisKanavos I'm not very familiar with thread in C#, would it be safe for example to call a write to a serial if that is on another thread ? – Damien Nov 16 '18 at 02:58
  • Qt timers and "events" run on event loops (message passing) and thus they are more robust than the C# ones – Aminos Apr 11 '23 at 20:43

3 Answers3

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.

Kzryzstof
  • 7,688
  • 10
  • 61
  • 108
  • `async void` is a bug waiting to happen, especially in this case. – Panagiotis Kanavos Nov 15 '18 at 12:55
  • @PanagiotisKanavos Fixed it :) – Kzryzstof Nov 15 '18 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 Nov 15 '18 at 13:32
  • @Damien Which solution overwrites the lambda? Both solutions (Panagiotis' and this one) seems to work properly, don't they? – Kzryzstof Nov 15 '18 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? – Zdeněk Jelínek Nov 15 '18 at 13:45
  • @ZdeněkJelínek In this scenario, awaiting the task to complete is not needed, so no, I do not think it is a problem (it may even be removed by the compiler - I am not sure of that though). Regarding what thread will be used, the default scheduler is used here which means it will run on a ThreadPool thread. – Kzryzstof Nov 15 '18 at 13:52
  • @Kzrystof I had this code sending data to the serial port, but if the serial is not opened an exception is caught and update a label on the UI with the exception message. This was causing a Thread Exception. – Damien Nov 16 '18 at 11:13
  • @Damien Could you add this piece of code so that I can adjust my answer to take into account the UI thread? – Kzryzstof Nov 16 '18 at 11:35
  • It was basically your code (on the Form code) calling a method which contained try { _serial.write(data); } catch (Exception ex) { labelError.Text = ex.Message.ToString(); } – Damien Nov 16 '18 at 11:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/183775/discussion-between-kzrystof-and-damien). – Kzryzstof Nov 16 '18 at 12:38
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();

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
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();.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172