Y'herd thisun? 

“Some critics of manned space flight on Capital Hill are said to be using their influence to hold down the amount of research on this subject [of Human-affecting radiation in Space]; this could be an indirect way of slowing the pace of flights to the moon and Mars.”

from Space Commerce, Materials Processing by Secretary of the Air Force, Dr. John L. McLucas

Wide interval timers

TaggedCoding, VB

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 ...



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
Return m_Interval
End Get
End Property
Public Property IsRunning() As Boolean
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
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), _
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:


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



home     who is smith    contact smith     rss feed π
Since 1997 a place for my stuff, and it if helps you too then all the better