Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Cool Blinkies

4.00/5 (5 votes)
2 Dec 2009CPOL7 min read 28.1K   347  
Grouped indicator lights that can be manipulated from multiple threads

Introduction

(Thanks to Gadwin PrintScreen for my screenshots. It is an excellent program.)

Cool Blinkies is a thread-safe and stable indicator blinky light group, that allows either multiple blinkies on:

VB.NET
CoolBlinkies1.A_AllowMultipleOn = True 

or a single blinky on:

VB.NET
CoolBlinkies1.A_AllowMultipleOn = False 

It has variable degrees of "latency":

VB.NET
CoolBlinkies1.A_Latency = uOneBlinky.LightLatencies.Medium 

According to this list:

VB.NET
Public Enum LightLatencies 
   None 
   [Short] 
   Medium 
   [Long] 
End Enum 

Latency allows the user to see that a blinky light went on, even if it was turned on only for an instant. If you need even more latency, you could look at the uOneBlinky:Private Sub SetLatency.

You set the number of blinky lights in your group:

VB.NET
CoolBlinkies1.A_NumberOfLights = 5 

The color of the blinkies:

VB.NET
CoolBlinkies1.A_LightColour = uOneBlinky.LightColours.Red 

According to this list:

VB.NET
Public Enum LightColours
   Red
   Blue
   Green
   Yellow
End Enum

Make it a row of blinky lights:

VB.NET
CoolBlinkies1.A_Orientation = Orientations.Horizontal

or a column of blinky lights as in the screenshot above.

VB.NET
CoolBlinkies1.A_Orientation = Orientations.Vertical

You set the margin between blinky lights:

VB.NET
CoolBlinkies1.A_Margin = 3

and whether a blinky light should stay on when you turn it on:

VB.NET
CoolBlinkies1.A_MomentaryOn = False 

or turn itself off after a period of time ("latency"):

VB.NET
CoolBlinkies1.A_MomentaryOn = True 

according to the A_Latency property described above.

If you use the A_MomentaryOn feature, you can also have the "first" or "zero" blinky light as a "default" blinky:

VB.NET
CoolBlinkies1.A_FirstLightIsOnByDefault = True 

In this case, the A_MomentaryOn property should be True. Then all you have to do is tell the group which blinky light to turn on:

VB.NET
A_LightState (Index) = uOneBlinky.LightStates.LightOn

That blinky light will then turn itself off following the latency period. And the "default" blinky light will go back on.

This can be used, for example, in a case where the text beside the "default" blinky light is something like "Idle". Normally you want that blinky light on steady, and when you want to show something "non-idle", you just turn the other blinky light on and do nothing more. After the latency period, the other blinky light will turn itself off, and the "default" blinky light will come back on.

Background

The challenge: Multiple threads controlling a row or column of indicator lights.

Seems simple, just turn them on and off, right? But sometimes the blinky light might get turned on and then off so quickly, that the user cannot even tell that the blinky light was ever on at all. So, you want a blinky light to stay on long enough for the human eye to notice it. That means delaying before drawing the blinky light in the "off colour". This is "latency" .

You shouldn't do such "delaying" on the same thread as the request, since that would delay that thread. Rather the blinky should receive the request to turn itself off, and allow another thread to do the delay, rather than blocking the calling thread. This allows the call to return immediately, with no delay.

After trying to do this with threading, I settled on doing it with Threading.Timer. (That is different from Windows.Forms.Timer, by the way.)

I have found the Threading.Timer to be reliable, accurate, and flexible. Each individual blinky light, when asked to turn itself off, uses (and reuses) an instance of Threading.Timer to delay for a length of time corresponding to its "Latency" property before redrawing itself in the "off colour".

Declare the Threading.Timer (and an infinite delay to prevent the timer from auto-repeating):

VB.NET
Private mLatencyTimer As Threading.Timer
Private mNoAutoRepeatInfiniteTimeSpan As TimeSpan _
  = New TimeSpan(Threading.Timeout.Infinite)

Set up the Threading.Timer:

VB.NET
Public Sub New( )

...

   'now initialize the latency timer. 
   'it will return and draw the light dark
   Dim QuickDelayForInitialSetup As New TimeSpan(100)
   
   mLatencyTimer = New Threading.Timer( _
      AddressOf LatencyTimerControl, Nothing, _
      QuickDelayForInitialSetup, _
      mNoAutoRepeatInfiniteTimeSpan)

End Sub

Here is the sub referenced by the Threading.Timer constructor:

VB.NET
Private Sub LatencyTimerControl()
   'when we get here, we need to turn 
   'the light off if we are still delaying towards off.
   If mCurrentLightState = LightStates.DelayingTowardsOff _
        Or mMomentaryOn = True Then

      'turn it off and paint it
      mCurrentLightState = LightStates.LightOff

      Invalidate()

      'Let parent know a light turned itself off,
      'in case it needs to turn on the DefaultLight
      RaiseEvent LatencyDone(mMyPosition)

   End If
End Sub

But with multiple threads potentially asking the same blinky light to turn on/off, there is another issue.

When one blinky light in a group turns on, the group must turn off the last blinky light that was on (unless you have set the AllowMultipleOn property to True). You want it to behave like a group of RadioButtons: If one is blinky light is "selected", the last one selected must be de-selected.

But in a case where many threads may be turning different blinky lights within a group, the "group" (the parent control, uCoolBlinkies) must remember the proper order to turn them off again. You can't store such values in a class-level Field, because a reentrant thread might change that value before it could be used to turn off the corresponding blinky light.

After much fiddling around with different ideas, I finally resolved to use a Queue (Of T) to store the indexes of the blinky lights that need to be turned off, and a BackGroundWorker to pull those indexes off the Queue and tell the corresponding blinky light to turn itself off.

Declare the Queue and BackgroundWorker:

VB.NET
Private mTurnOffQueue As New Queue(Of Integer)
Private WithEvents mTurnOffBW As BackgroundWorker

Set up the BackgroundWorker and arrange for its shutdown:

VB.NET
Private Sub uCoolBlinkies_Load _
    (ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles Me.Load

   mTurnOffBW = New BackgroundWorker
   mTurnOffBW.WorkerSupportsCancellation = True
   mTurnOffBW.RunWorkerAsync()

End Sub

Private Sub uCoolBlinkies_Disposed _
    (ByVal sender As Object, ByVal e As System.EventArgs) _
    Handles Me.Disposed

   mStopBW = True
   mTurnOffBW.CancelAsync()

End Sub

And here's how to link it to the sub that actually does the work:

VB.NET
Private Sub mTurnOffBW_DoWork(ByVal sender As Object, _
    ByVal e As DoWorkEventArgs) _
    Handles mTurnOffBW.DoWork

   Dim BW As BackgroundWorker = CType(sender, BackgroundWorker)
   CheckForTurnOffs(BW)

End Sub

Private Sub CheckForTurnOffs(ByVal ABW As BackgroundWorker)
   Do
      If mTurnOffQueue.Count = 0 Then
         Thread.Sleep(100)
      Else
         mLights(mTurnOffQueue.Dequeue).A_LightState = _
            uOneBlinky.LightStates.LightOff
      End If
   Loop Until mStopBW
End Sub

The above sub CheckForTurnOffs works continually, either sleeping if no items are on the Queue, or Dequeueing the index of a light that needs turning off and telling the light to turn itself off.

When a light needs turning on, you tell it to turn on, and then you might have to tell another light to turn off if you are not allowing multiple lights to be on. Below I turn a light on and then turn the last light off, unless it has not yet been defined (Index = -1). To turn the light off, I just pop the index of the light onto the queue, and forget about it. Once it is Queued, I can update the mLastLightOn to the current light I just turned on.

VB.NET
'turn the new light on
mLights(Index).A_LightState = uOneBlinky.LightStates.LightOn

'now set up for turning it off.

If Not mAllowMultipleOn Then

   If mLastLightOn > -1 Then mTurnOffQueue.Enqueue(mLastLightOn)
   mLastLightOn = Index

End If

Using the Code

I suggest running the project. (Make sure the Tester project is the "Startup Project"). Almost all of the properties are demonstrated on the user interface.

As you play with the demo, keep in mind that in your project, the blinky lights won't be controlled with a TrackBar. The TrackBar is there to let you turn blinky lights on and off.

Right now, the Blinky Group (a "row" or "column" of blinky lights) is a UserControl called uCoolBlinkies. It is the parent control. (I suppose I could have made it a Component based on Panel or something, but I didn't yet try that.)

uCoolBlinkies owns a List (Of T) individual uOneBlinky.

uOneBlinky knows how to repaint itself as an "on" light or an "off" light, using red, yellow, blue or green colour. When it's told to turn itself off, it knows how to set its timer to allow latency before repainting in the "off" colour.

uCoolBlinkies remembers which lights have been on and which need turning off, in the correct order.

I have all public properties and methods prefixed with "A_" so they go to the top of the intellisense list, and are not mixed in with other items. Anyone have a better way of grouping custom properties/methods?

To use uCoolBlinkies in your projects, just add the two UserControls, uCoolBlinkies.vb and uOneBlinky.vb to your project. uCoolBlinkies should then appear (maybe after a Rebuild) in your Toolbox, and you can drag one or more to your form. (Don't drag uOneBlinky to your form. It is used only by uCoolBlinkies.)

Points of Interest

Use of a Queue and BackgroundWorker to keep track of the order of events when accessed by multiple threads. Maybe someone out there could suggest an improvement to this.

Use Threading.Timer to reliably delay a repaint event (to turn the light off), which doesn't keep a thread waiting.

History

  • This is submitted first here November 30, 2009, but has been in development on and off for over 10 years. I had a lot of trouble in my multi-threaded application, there were bizarre timing issues, and it all came down to this little wee tiny blinky light not delaying properly! Now that I think I've beaten that issue and made it thread-safe, I am submitting this for use by others, and if any of the community sees any glaring or even subtle problems with my use of Queue and Threading.Timer, I welcome your comments and contributions.
  • Dec 2, 2009 - Editing clarifications made, no programming changes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)