Hi Folks,

I have created a windows form control derived from the ComboBox to allow visual feedback from the control to confirm if the required change is complete or not after the SelectedIndex is changed. This is because the change actually sends data to an external device by writing to the serial port and then reading back the same item to confirm the change. If the data on the device has not been changed as intended the user needs to know about that.

The first part of this visual feedback is undoing the SelectedIndex change. So the derived control always saves the current index. It then overrides the SelectedIndexChanging event, raising a SelectedIndexChanging event in its place. This allows the system to cancel the event if the change was not confirmed which then replaces the selected index with the previously saved, old, index, undoing the change.

This works fine, but with lots of controls on a form it's hard to notice that one in particular hasn't changed as a user thought it should.

Thus I added in a color change and fade-out to the BackColor. Now when a change is made and is not cancelled the background of the control changes to green, then fades back to white. If it is cancelled then the background changes to red before also fading back to white.

The fading is achieved by adding a BackgroundWorker to the control which should complete when the color has faded to white. So far, so good. The problem comes in however when a user closes the application and the BackgroundWorker is still running, then the ReportProgress() method call causes an InvalidOperationException with the message "Invoke or BeginInvoke cannot be called on a control until the window handle has been created.". I assume this is because the BackgroundWorker is trying to call an Invoke type method on the control that, at this point, has been disposed.

I tried several avenues of attack to overcome the issue: an Applications.DoEvents() in the form's FormClosing handler (this seemed to help somewhat but not completely), adding multiple checks for CancellationPending in the DoWork event of the BackgroundWorker, but nothing solve the issue completely.

I imagine I can add a Dispose() override to the derived control to wait for the BackgroundWorker to complete before calling MyBase.Dispose() might prevent the exception but what is the correct and reliable way to do that? And how can I avoid the BackgroundWorker taking ages to actually complete and thus noticeably affecting my applications close time?
Or is there a better way to have the BackgroundWorker work?

(For this minor BackgroundWorker work I wasn't really interested in switching to using threads directly unless achieving this using a BackgroundWorker would prove prohibitive.)

The derived control class code:

vb Code:
  1. Public Class BetterComboBox
  2.     Inherits ComboBox
  3.  
  4.     Public Event SelectedIndexChanging As System.ComponentModel.CancelEventHandler 'Create a new event
  5.  
  6.     Private WithEvents FadingWorker As New System.ComponentModel.BackgroundWorker
  7.     Private _lastIndex As Integer = -1
  8.     Private Const _fade As Double = 17
  9.     Private Const _fadeInterval As Double = 150
  10.  
  11.     <System.ComponentModel.Browsable(False)>
  12.     Public ReadOnly Property LastIndex As Integer
  13.         Get
  14.             Return _lastIndex
  15.         End Get
  16.     End Property
  17.  
  18.     Public Sub New()
  19.         MyBase.New()
  20.         _lastIndex = -1 'Initialise the last index as the default first index.
  21.         FadingWorker.WorkerReportsProgress = True
  22.         FadingWorker.WorkerSupportsCancellation = True
  23.     End Sub
  24.  
  25.     Protected Overridable Sub OnSelectedIndexChanging(e As System.ComponentModel.CancelEventArgs)
  26.         RaiseEvent SelectedIndexChanging(Me, e) 'Raise the new event
  27.     End Sub
  28.  
  29.     Protected Overrides Sub OnSelectedIndexChanged(e As System.EventArgs)
  30.         If _lastIndex <> SelectedIndex Then
  31.             Dim cancelEventArgs As New System.ComponentModel.CancelEventArgs()
  32.             OnSelectedIndexChanging(cancelEventArgs) 'Raise the new event
  33.  
  34.             If cancelEventArgs.Cancel = False Then 'Check the response from the event
  35.                 _lastIndex = SelectedIndex 'Save the index change
  36.                 BackColor = Color.FromArgb(0, &HFF, 0) 'Change BG color to green
  37.                 If Not FadingWorker.IsBusy Then 'Check the worker isnt already started
  38.                     FadingWorker.RunWorkerAsync(Me) 'Start the worker
  39.                 End If
  40.                 MyBase.OnSelectedIndexChanged(e) 'Raise the normal event
  41.             Else
  42.                 BackColor = Color.FromArgb(&HFF, 0, 0) 'Change BG color to red
  43.                 If Not FadingWorker.IsBusy Then 'Check the worker isnt already started
  44.                     FadingWorker.RunWorkerAsync(Me) 'Start the worker
  45.                 End If
  46.                 SelectedIndex = _lastIndex 'undo the index change
  47.             End If
  48.         End If
  49.     End Sub
  50.  
  51.     Private Sub FadingWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles FadingWorker.DoWork
  52.         Dim worker As System.ComponentModel.BackgroundWorker = CType(sender, System.ComponentModel.BackgroundWorker)
  53.         While Not worker.CancellationPending 'Check the worker wasn't cancelled during progress reporting
  54.             System.Threading.Thread.Sleep(_fadeInterval) ' Wait N milliseconds
  55.             If Not worker.CancellationPending Then 'Check the worker wasn't cancelled during the delay
  56.                 FadingWorker.ReportProgress(0, e.Argument) ' Signal the main thread to adjust the color
  57.             End If
  58.         End While
  59.     End Sub
  60.  
  61.     Private Sub FadingWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles FadingWorker.ProgressChanged
  62.         Dim cb As BetterComboBox = CType(e.UserState, BetterComboBox)
  63.        
  64.         ' Code that fades the BackgroundColor RGB value here
  65.  
  66.         If r = g = b = &HFF Then 'Check if the BackColor has reached white
  67.             FadingWorker.CancelAsync() 'Cancel the BackgroundWorker
  68.         End If
  69.     End Sub
  70.  
  71.     Protected Overrides Sub Dispose(ByVal disposing As Boolean)
  72.         'Use this to wait for the background worker to be die before disposing
  73.         If Not Me.IsDisposed Then
  74.             If disposing Then
  75.                 'Free managed resources here
  76.             End If
  77.             'Free unmanaged resources here
  78.         End If
  79.         MyBase.Dispose(disposing)
  80.     End Sub
  81. End Class

PS: Also, is a BackgroundWorker considered managed or unmanaged?

Many thanks!! T