Multiple DispatcherTimers with one tick eventhandler
-
14-04-2021 - |
Question
I'm working on a project that will require me to react to 5 separate events generated by an external device and then do something after a certain delay. The events will normally happen one at a time but may occasionally be simultaneous. Is this a bad idea and if so why?
Imports System.Windows.Threading
Class MainWindow
Private TimerList As List(Of DispatcherTimer)
Private Sub Window_Loaded(ByVal sender As System.Object,
ByVal e As ystem.Windows.RoutedEventArgs) Handles MyBase.Loaded
TimerList = New List(Of DispatcherTimer)
'Create 5 timers for the 5 events from external device
For i As Integer = 1 To 5
TimerList.Add(
New DispatcherTimer(TimeSpan.FromSeconds(10), DispatcherPriority.Normal,
New System.EventHandler(AddressOf tmr_Tick),
Me.Dispatcher) With {.Tag = i}
)
Next
End Sub
Public Sub SomeEventFromExternalDevice(ByVal ID As Integer) 'handles...
Dim CurTimer = TimerList.Find(Function(x) x.Tag = ID)
If Not CurTimer Is Nothing Then
CurTimer.Start()
End If
End Sub
Private Sub tmr_Tick(ByVal sender As Object, ByVal e As System.EventArgs)
Dim ID = DirectCast(sender.tag, Integer)
Dim curTimer = TimerList.Find(Function(x) x.Tag = ID)
curTimer.Stop()
Select Case ID
'change something on the UI to indicate that event x timer has elapsed
End Select
End Sub
End Class
Solution
Probably a better solution to this is to get rid of the DispatcherTimer altogether and do this with plain threads.
First create a class to hold a single event work package - I've added a way to pass the delay value here but you can omit it if it doesn't fit your needs. This is a simple procedure which sleeps the thread for the delay and then raises a new event for you to catch. If you need precision then you can implement the delay with a Stopwatch or something else:
Public Class DelayThread
' Task information
Private _ID As Integer
Private _delay As Integer
Event onDelayUp(ByVal ID As Integer)
' Constructor
Public Sub New(ByVal myID As Integer, ByVal myDelay As Integer)
_ID = myID
_delay = myDelay
End Sub
Public Sub DelayProc()
System.Threading.Thread.Sleep(_delay)
RaiseEvent onDelayUp(_ID)
End Sub
End Class
Now your device will fire events handled here:
Public Sub SomeEventFromExtDevice(ByVal ID As Integer) 'Handles ...
'get a "delay" value from somewhere, if you like
Dim newDelayTask As New DelayThread(ID, delay) 'added delay here
AddHandler newDelayTask.onDelayUp, AddressOf DoSomething
Dim t As New System.Threading.Thread( _
New System.Threading.ThreadStart(AddressOf newDelayTask.DelayProc))
t.Priority = Threading.ThreadPriority.Normal 'whatever priority
t.Start()
End Sub
This way, each time an event fires you start a new thread which waits for your delay time, does DoSomething and then terminates, cleaning itself up in the process.
Here you will need some "DoSomething" procedure :
'declare this somewhere
Delegate Sub dlgDoSomething(ByVal ID As Integer)
Public Sub DoSomething(ByVal ID As Integer) 'hooked to onDelayUp @ runtime above
'using "Me" here - if DoSomething is somewhere else
'you may need to refer to the main UI form instead
Me.Invoke(New dlgDoSomething(AddressOf uiDoSomething), New Object() {ID})
End Sub
The DoSomething procedure will be called from each thread so it has to invoke on the main UI thread - then need :
Public Sub uiDoSomething(ByVal ID As Integer)
' Do Something with ID - UI thread is now executing this so you are safe
End Sub
If knowing the exact order of the events is important - knowing when they arrived and in what order - then you could add a timestamp in the SomeEventFromExtDevice and pass that along also.
You might also want to add some handling for shutting down the application - there are no checks here to make sure that threads are not trying to marshall onto the main form after it has been disposed, for example.