since 1997 a place for my stuff, and if it helps you then so much the better

 
...
...

Wide interval timers


We have a WinForms program doing DataWarehouse report functions and need to include the option of firing off events at pretty long intervals (hours, and even days between them) and want to be able to tell the user that the app is still running with a bit of detail. I was thinking of showing a countdown in a label but the DateAdd functions are confusing me. Do you have any pointers?

What you're looking for isn't all that difficult, depending on the accuracy you require. I do have to say that you might want to look into serverside timing and having the desktops check the status of reports rather than kicking off the processing from a user GUI; Having a server manage the timing is probably going to be a little safer than asking users to keep their machines on for days at a stretch ... but that's up to you.

Even if you do rethink the architecture of this project, the trick you're looking for can be encapsulated in a class and used any time you want to have a repeated countdown. There are some things to consider however, so always talk to your PM or users and find out exactly what their needs are, not just what they think they want.


Dealing with time is always full of gotchas, it's like the fortune cookie said: "Man with one watch knows what time it is, man with two watches is never sure." There are so many variables that can impact a timer such as a user running a bunch of heavy apps .. and either playing with their system clock or having an automatic time syncher updating their clock automatically. Plus, there's the drift of the various timers you can pick from (System timer = up to 50+ms latency, QueryPerformanceCounter = reportedly around 10ms, Multimedia timer down to 1ms accuracy but a bit tough to implement).

DotNet2 has a cool new Stopwatch class that automatically picks a high performance counter if the OS has one (looks like it goes for the PerformanceCounter) and if not then it uses the System timer, so tick purists are getting a little more out of the abstraction layer, but till then we can keep things simple and get pretty close, perceptually anyway, by watchdogging the system timer.

Add the class below to your Winforms project ...

 Option Strict On
 Imports Microsoft.Win32
 
 Public Class CountDowner
 
    Private m_Timer As Timer
    Private m_Interval As TimeSpan
    Private m_NextFireEvent As DateTime
    Private m_LastTimerDateTime As DateTime = DateTime.MinValue
    Private m_SysClockChange As Boolean = False
 
 Public Event TMinus(ByVal D as short, ByVal H As Short, _
       ByVal M As Short, ByVal S As Short, _
       ByVal NextFire As DateTime)
 
    Public Event FireEvent()
 
    Public Sub New(ByVal Interval As TimeSpan)
       m_Timer = New Timer
       m_Interval = Interval
    End Sub
 
    Public ReadOnly Property Interval() As TimeSpan
       Get
          Return m_Interval
       End Get
    End Property
 
    Public Property IsRunning() As Boolean
       Get
          Return m_Timer.Enabled
       End Get
       Set(ByVal Value As Boolean)
          If Value And Not m_Timer.Enabled Then
             AddHandler m_Timer.Tick, AddressOf HandleTimer
             m_Timer.Interval = 1000
             m_NextFireEvent = Now.Add(m_Interval)
             m_LastTimerDateTime = Now
             m_Timer.Enabled = True
             AddHandler SystemEvents.TimeChanged, AddressOf SysClockChanged
 
             Exit Property
          ElseIf Not Value And m_Timer.Enabled Then
             m_Timer.Enabled = False
             RemoveHandler m_Timer.Tick, AddressOf HandleTimer
             RemoveHandler SystemEvents.TimeChanged, AddressOf SysClockChanged
 
          End If
       End Set
    End Property
 
    Private Sub HandleTimer(ByVal s As Object, ByVal e As EventArgs)
       Dim iSeconds As Integer
       Dim iMinutes As Integer
       Dim iHours As Integer
       Dim iDays As Integer
       If m_SysClockChange = False Then
          m_LastTimerDateTime = Now
       Else
          Dim SecsDifferent As Integer
          SecsDifferent = CInt(DateDiff(DateInterval.Second, _
             m_LastTimerDateTime, Now))
          m_LastTimerDateTime = DateAdd(DateInterval.Second, _
             SecsDifferent, m_LastTimerDateTime)
          m_NextFireEvent = DateAdd(DateInterval.Second, _
             SecsDifferent, m_NextFireEvent)
          m_SysClockChange = False
       End If
 
       Dim totalSecs As Integer = _
          CInt(DateDiff(DateInterval.Second, m_LastTimerDateTime, m_NextFireEvent))
       iSeconds = totalSecs
       iDays = iSeconds \ 86400
       iSeconds = iSeconds - (iDays * 86400)
       iHours = iSeconds \ 3600
       iSeconds = iSeconds - (iHours * 3600)
       iMinutes = iSeconds \ 60
       iSeconds = iSeconds - (iMinutes * 60)
       If m_NextFireEvent <= Now Then
          RaiseEvent FireEvent()
          m_NextFireEvent = m_NextFireEvent.Add(m_Interval)
       End If
       RaiseEvent TMinus(CShort(iDays), CShort(iHours), _
 	   CShort(iMinutes), CShort(iSeconds), _
            m_NextFireEvent)
    End Sub
 
    Private Sub SysClockChanged(ByVal sender As Object, ByVal e As EventArgs)
       m_SysClockChange = True
    End Sub
 
 End Class

Essentially this class encapsulates a timer object (not a Forms timer). You set the interval in the constructor using a TimeSpan and when the IsRunning property is set to True it starts the timer.

It's hard coded to tick at 1000ms so approximately every second the HandleTimer sub is called. That sub looks at the previously extrapolated m_NextFireEvent variable and if the current system time is equal to or later than that value then the FireEvent event is raised and the m_NextFireEvent variable is reset to the current system time plus the TimeSpan. Further, the sub calculates the days, hours, minutes and seconds between the current systemtime and the extrapolated next fire time and tosses that information up to the parent using the TMinus event.

Covering the base of a user playing with their system clock (or a corporate or downloaded clock setter running), when the timer is started we hook into the Win32.SystemEvents.TimeChanged event. If the event fires we set a module level flag that the timer will check in its' processing. We compare the last noted system datetime against the current system datetime and adjust the m_NextFireEvent accordingly. With these few lines we know that the timer interval will be correct relative to the last time it fired no matter what the system clock happens to say. Yes, this does impact the accuracy of the processing slightly, perhaps by a second or so becuase of the 1000ms click, but it's to account for fiddling outside the control of the app so the root cause is far more impactive than any minor deviation side effects of the recalculation.

To use the class, just add it to your project and attach it to your form with a WIthEvents declaration as in:

private WithEvents m_Countdowner as Countdowner

In the Class combo, pick the m_Countdowner and drop down the Method Name combo to get at events. Add your code to the TMinus event to update your labels and add code to the FireEvent event to start the timed actions.

Add a button to your form (or however you want to start the timer) and in its click add code to create a New instance (the constructor sets the TimeSpan) and set IsRunning to True.

You've noticed that I made the Interval property ReadOnly, which means that the only way to set the interval is by via the constructor, requiring the creation of a new instance. Feel free to add a setter if you need to after talking to your users about how their events are to be handled. Changing the Interval while the timer is running will mean recalculating the various internal properties and stopping and restarting the timer so you'll have to decide whether to trip the next pending event and then do the switch or dump the pending event and start anew from the current second ... maybe the choice should be dependant on how close you are to the next pending event (hmm, that's a nice bit of logic to ponder).

Hope it helps!

Robert Smith
Kirkland, WA

added to smithvoice November 2004


...
...

"In theory, theory and practice are the same. In practice, they are not." -Albert Einstein