58

I would like to have an event triggered in my app which runs continuously during the day at a certain time, say at 4:00pm. I thought about running the timer every second and when the time is equal to 4:00pm run the event. That works. But I'm wondering if there's a way to just get the callback once at 4:00pm and not having to keep checking.

Kay Lee
  • 922
  • 1
  • 12
  • 40
Behrooz Karjoo
  • 4,250
  • 10
  • 38
  • 48

9 Answers9

56

How about something like this, using the System.Threading.Timer class?

var t = new Timer(TimerCallback);

// Figure how much time until 4:00
DateTime now = DateTime.Now;
DateTime fourOClock = DateTime.Today.AddHours(16.0);

// If it's already past 4:00, wait until 4:00 tomorrow    
if (now > fourOClock)
{
    fourOClock = fourOClock.AddDays(1.0);
}

int msUntilFour = (int)((fourOClock - now).TotalMilliseconds);

// Set the timer to elapse only once, at 4:00.
t.Change(msUntilFour, Timeout.Infinite);

Note that if you use a System.Threading.Timer, the callback specified by TimerCallback will be executed on a thread pool (non-UI) thread—so if you're planning on doing something with your UI at 4:00, you'll have to marshal the code appropriately (e.g., using Control.Invoke in a Windows Forms app, or Dispatcher.Invoke in a WPF app).

Dan Tao
  • 125,917
  • 54
  • 300
  • 447
  • Sounds good but there's a problem. , but t.Change(timeUntilFour, Timeout.Infinite); gives an error saying there's no overload for timespan, timespan. I see in the docs I can also call dispose but not sure where. Any ideas? – Behrooz Karjoo Dec 25 '10 at 02:42
  • this worked: t.Change(timeUntilFour, new TimeSpan(Timeout.Infinite)); – Behrooz Karjoo Dec 25 '10 at 03:29
  • @Behrooz: Sorry, I mixed up the overload that accepts `TimeSpan` parameters and the one that accepts `int` parameters. I think my updated code is a better alternative to the `new TimeSpan(Timeout.Infinite)` one (though maybe it doesn't really matter). – Dan Tao Dec 25 '10 at 04:25
  • 1
    And will that correctly work in the presence of hibernate, standby,...? – CodesInChaos Dec 25 '10 at 12:51
  • Not concerned about hibernate and standby as they are shut off in our environment. – Behrooz Karjoo Dec 25 '10 at 18:37
  • I don't know if it existed before, but I use System.Threading.Timeout.InfiniteTimeSpan – VoteCoffee Jan 08 '15 at 02:57
  • Won't `Timeout.Infinite` cause the timer to signal only one time? For a daily signal, isn't `new TimeSpan(24, 0, 0);` appropriate? – Jason Mar 26 '18 at 15:27
  • 4
    This code assumes 16 hours from midnight until 4:00 pm. That assumption is invalid during daylight saving time transitions. – Edward Brey Jan 23 '19 at 14:29
  • 2
    The code also assumes that the system clock is always accurate. If there's a mistake and it needs to be adjusted, the code doesn't detect that and adjust its time remaining on `t` accordingly. – Edward Brey Jan 23 '19 at 14:31
35

Starting with .NET 4.5 there's a really clean solution:

public async void ScheduleAction(Action action, DateTime ExecutionTime)
{
    await Task.Delay((int)ExecutionTime.Subtract(DateTime.Now).TotalMilliseconds);
    action();
}

Here's a solution without async/await:

public void Execute(Action action, DateTime ExecutionTime)
{
    Task WaitTask = Task.Delay((int)ExecutionTime.Subtract(DateTime.Now).TotalMilliseconds);
    WaitTask.ContinueWith(_ => action);
    WaitTask.Start();
}

It should be noted that this only works for about 24 days out because of int32 max value, which is plenty for your case, but worth noting.

VoteCoffee
  • 4,692
  • 1
  • 41
  • 44
  • 2
    You can actually use a TimeSpan now for Task.Delay, meaning you can go over the 24 day limit mentioned by VoteCoffee. There's essentially no limit with TimeSpan, something like 10 million days, https://msdn.microsoft.com/en-us/library/system.timespan.maxvalue(v=vs.110).aspx – chris84948 Mar 14 '16 at 14:37
  • 2
    Nice solution, but "ExecutionTime" must be in the future or Task.Delay will throw an ArgumentOutOfRangeException. – Rob Davis Jun 16 '16 at 22:24
  • 3
    Beware from using `async void`, an exception in `action()` will cause the entire application to crash. Read more: http://haacked.com/archive/2014/11/11/async-void-methods/ – NucS Aug 18 '17 at 15:38
  • 2
    @chris84948 even if you give it a timespan, `Task.Delay(Date.Subtract(DateTime.Now));` it will throw AurgumentOutOfRange when exceeding int32.MaxValue. – Guy Nov 06 '17 at 14:02
  • This seems a little dangerous to me. I think you should add a check that the calculated number of milliseconds is positive. Especially since [`Task.Delay(-1)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay?view=netframework-4.8) will never complete and other negative values will throw. – binki Apr 26 '19 at 13:48
  • It's a good suggestion and something people should be aware of. I generally post the relevant piece of code, and not necessarily all of the associated overhead like error handling/input checking/etc. – VoteCoffee Apr 26 '19 at 19:40
  • This approach assumes among [other things](https://stackoverflow.com/a/55866122/145173) that the computer doesn't enter standby; otherwise, its wake-up time will be incorrect. – Edward Brey Mar 28 '23 at 11:35
11

Taking VoteCoffees lead, here is a compact event based solution:

/// <summary>
/// Utility class for triggering an event every 24 hours at a specified time of day
/// </summary>
public class DailyTrigger : IDisposable
{
    /// <summary>
    /// Time of day (from 00:00:00) to trigger
    /// </summary>
    TimeSpan TriggerHour { get; }

    /// <summary>
    /// Task cancellation token source to cancel delayed task on disposal
    /// </summary>
    CancellationTokenSource CancellationToken { get; set; }

    /// <summary>
    /// Reference to the running task
    /// </summary>
    Task RunningTask { get; set; }

    /// <summary>
    /// Initiator
    /// </summary>
    /// <param name="hour">The hour of the day to trigger</param>
    /// <param name="minute">The minute to trigger</param>
    /// <param name="second">The second to trigger</param>
    public DailyTrigger(int hour, int minute = 0, int second = 0)
    {
        TriggerHour = new TimeSpan(hour, minute, second);
        CancellationToken = new CancellationTokenSource();
        RunningTask = Task.Run(async () => 
        {
            while (true)
            {
                var triggerTime = DateTime.Today + TriggerHour - DateTime.Now;
                if (triggerTime < TimeSpan.Zero)
                    triggerTime = triggerTime.Add(new TimeSpan(24, 0, 0));
                await Task.Delay(triggerTime, CancellationToken.Token);
                OnTimeTriggered?.Invoke();
            }
        }, CancellationToken.Token);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        CancellationToken?.Cancel();
        CancellationToken?.Dispose();
        CancellationToken = null;
        RunningTask?.Dispose();
        RunningTask = null;
    }

    /// <summary>
    /// Triggers once every 24 hours on the specified time
    /// </summary>
    public event Action OnTimeTriggered;

    /// <summary>
    /// Finalized to ensure Dispose is called when out of scope
    /// </summary>
    ~DailyTrigger() => Dispose();
}

Consumer:`

void Main()
{
    var trigger = new DailyTrigger(16); // every day at 4:00pm

    trigger.OnTimeTriggered += () => 
    {
        // Whatever
    };  
    
    Console.ReadKey();
}
noontz
  • 1,782
  • 20
  • 29
7

You can use Task Sceduler on windows See daily trigger example for detail.

or use bellow code if you want wrote it yourself:

public void InitTimer()
{
    DateTime time = DateTime.Now;
    int second = time.Second;
    int minute = time.Minute;
    if (second != 0)
    {
        minute = minute > 0 ? minute-- : 59;
    }

    if (minute == 0 && second == 0)
    {
        // DoAction: in this function also set your timer interval to 24 hours
    }
    else
    {
        TimeSpan span = //new daily timespan, previous code was hourly: new TimeSpan(0, 60 - minute, 60 - second);
        timer.Interval = (int) span.TotalMilliseconds - 100; 
        timer.Tick += new EventHandler(timer_Tick);
        timer.Start();
    }
}

void timer_Tick(object sender, EventArgs e)
{
    timer.Interval = ...; // 24 hours
    // DoAction
}
Saeed Amiri
  • 22,252
  • 5
  • 45
  • 83
  • @Øyvind Bråthen, I wrote this for hourly (before), and you can see it in comments, You can use DateTime.Today.AddHours(16) for 16, but also you can use it if DateTime.Now is 16:00 in your interval which will be called every hour, Also my main focus was using windows task scheduler instead of creating the weal. (I don't like to edit the code, I'll left it as homework :D, Dan code is similar to this) – Saeed Amiri Dec 25 '10 at 08:00
4

.NET has lots of timer classes, but they all take time spans relative to the current time. With a relative time, there are lots of things to consider before you start your timer and to monitor for while your timer is running.

  • What if the computer goes into a standby state?
  • What if the computer's time changes?
  • What if the expiration time is after a daylight savings time transition?
  • What if the user changes the computer's time zone after you started your timer such that there is a new time zone transition before expiration that wasn't previously there?

The operating system is in a great place to handle this complexity. Application code running on .NET is not.

For Windows, the NuGet package AbsoluteTimer wraps an operating system timer that expires at an absolute time.

Edward Brey
  • 40,302
  • 20
  • 199
  • 253
3

I did this way to fire 7am every morning

bool _ran = false; //initial setting at start up
    private void timer_Tick(object sender, EventArgs e)
    {

        if (DateTime.Now.Hour == 7 && _ran==false)
        {
            _ran = true;
            Do_Something();               

        }

        if(DateTime.Now.Hour != 7 && _ran == true)
        {
            _ran = false;
        }

    }
Jon649
  • 279
  • 3
  • 7
  • In ways this is the easiest way to make something run on a daily basis at a certain time. I use this style for daily housekeeping in some Services IMO. – Miguelito Dec 09 '19 at 15:05
  • This solution is awesome, It doesn't even need to be linked to a timer control. Every time any event is fired, it will check the time and decide if triggering the Do_Something is necessary or if it was already done. Excellent solution. – Felipe La Rotta Aug 26 '22 at 01:39
0

Echo to Dan's solution, using Timercallback is a quick and neat solution. Inside the method you want to schedule a task or subroutine to be run, use the following:

    t = New Timer(Sub()
                        'method call or code here'
                  End Sub, Nothing, 400, Timeout.Infinite)

use of 'Timeout.Infinite' will ensure the callback will be executed only once after 400ms. I am using VB.Net.

Joseph Wu
  • 4,786
  • 1
  • 21
  • 19
0

Task scheduler is a better option, and it can be easily used in C#, http://taskscheduler.codeplex.com/

Lex Li
  • 60,503
  • 9
  • 116
  • 147
0

How about this solution?

Sub Main()
  Dim t As New Thread(AddressOf myTask)
  t.Start()
  Console.ReadLine()
End Sub

Private Sub myTask()
  Dim a = "14:35"
  Dim format = "dd/MM/yyyy HH:mm:ss"
  Dim targetTime = DateTime.Parse(a)
  Dim currentTime = DateTime.Parse(Now.ToString(format))
  Console.WriteLine(currentTime)
  Console.WriteLine("target time " & targetTime)
  Dim bb As TimeSpan = targetTime - currentTime
  If bb.TotalMilliseconds < 0 Then
    targetTime = targetTime.AddDays(1)
    bb = targetTime - currentTime
  End If
  Console.WriteLine("Going to sleep at " & Now.ToString & " for " & bb.TotalMilliseconds)
  Thread.Sleep(bb.TotalMilliseconds)
  Console.WriteLine("Woke up at " & Now.ToString(format))
End Sub
LarsTech
  • 80,625
  • 14
  • 153
  • 225
Tal Aruety
  • 23
  • 5