Ok, so let's see where we are. In Post1 I talked about getting an animated gif on the screen to indicate the machine is thinking. After struggling through my own pigheadedness, I got that working - almost. It was basically a complete flop because the application was so busy processing that it couldn't bother to update the animated gif indicating that it was processing. That lead me to Post2, where I explored the mirky world of function pointers. I'm calling them function pointers and not delegates because that's how I was using them. I got one of the concepts right, but not the one that was important to my task.
That leads me to this post, where I follow Ted Pattison's advice and create a second delegate to pass control from my secondary thread back to my primary thread (50ยข word here - this is called Marshalling Across Thread Boundries). Thanks go out to Shawn Wildermuth for pointing this article out to me.
When I left off, I had sucessfully created a delegate and was using that delegate to allow a function to callback to the form object. Of course, I was still on a single thread so this was completely useless to me. To get things going on another thread, I needed to call either the delegate's Invoke or BeginInvoke method. The difference between the two is that the call to Invoke is a blocking call (which requires a second call to the delegate to get results) while BeginInvoke is not (it uses callback functionality). I'm going to use BeginInvoke.
I've still got my delegate but notice that I've now changed it from a Sub to a Function which returns a BinsCollection. This is going to be important:
Public Delegate Function FillBinsCallback() As BinsCollection
Now I'm creating member variables in the form class and assigning the addresses ahead of time:
' delegate object to execute method asynchronously
Private BinFillerAsynch As FillBinsCallback = AddressOf BuildValidBinsCollection
' delegate object to service callback from CLR
Private CallbackHandler As AsyncCallback = AddressOf DatabindBinsList
My function called by the user's button click looks similar to before, except now, instead of calling out to a new class, I'm directly calling the BinFillerAsynch delegate object's BeginInvoke method. This causes a worker thread to come out of the CLR's thread pool and the new worker thread is what calls BuildValidBinsCollection():
Public Sub PopulateBinsList()
' calculate capacity
If (IncludeRecallsCheckBox.Checked) Then
_binCapacity = EnteredItem.WarehouseRecords(0).onHand + _
EnteredItem.WarehouseRecords(0).onOrder + _
EnteredItem.InRecall
Else
_binCapacity = EnteredItem.WarehouseRecords(0).onHand + _
EnteredItem.WarehouseRecords(0).onOrder
End If
' get valid bins
Windows.Forms.Cursor.Current = Cursors.WaitCursor
Busy = True
cboAssignBin.Text = "Working...Please wait"
' call BeginInvode on the BinFillerAsynch to create our collection of valid bins
BinFillerAsynch.BeginInvoke(CallbackHandler, BinFillerAsynch)
End Sub
I've gotten rid of the other class I'd previously created (my BinFiller class) so the BuildValidBinsCollection (now) function is now part of the form class. It now looks like this:
Public Function BuildValidBinsCollection() As BinsCollection
BuildValidBinsCollection = _validBins.GetValidBins(EnteredItem.AlphaCfg, _
_warehouseNumber, _
_binCapacity)
End Function
Now, when the thread is done with it's work it will call back to the callback method I've passed it (DatabindBinsList). DatabindBinsList can't actually modify the form's controls because that wouldn't be a thread-safe operation. Instead we're going to call out to another method to update the UI for us, and we'll pass back my all-important BinsCollection. Notice that I'm getting my BinsCollection when I call the EndInvoke method on my delegate.
Public Sub DatabindBinsList(ByVal ar As IAsyncResult)
Try
Dim ValidBins As BinsCollection
ValidBins = BinFillerAsynch.EndInvoke(ar)
UpdateUI("Complete", ValidBins)
Catch ex As Exception
Dim msg As String = "Error in DatabindBinsList: " & ex.Message.ToString
UpdateUI(msg, Nothing)
End Try
End Sub
To update the UI, I actually need to call another BeginInvoke function. This time however, I'm going to be calling BeginInvoke on the form itself. When you call BeginInvoke on a form or form control, you pass back to the primary thread (actually, your payload is marshalled across thread boundries) where updating the UI is perfectly safe. To accomplish all of this, I'm going to need a new delegate:
Public Delegate Sub UpdateUIHandler(ByVal statusMessage As String, ByRef validBins As BinsCollection)
And the function itself:
Public Sub UpdateUI(ByVal statusMessage As String, ByRef validBins As BinsCollection)
' switch control back to the main thread (to update the UI
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {statusMessage, validBins}
' call BeginInvoke method of the form object
Me.BeginInvoke(handler, args)
End Sub
When I call Me.BeginInvoke (referencing the form), the main thread is going to call the function I pass in:
Public Sub UpdateUI_Impl(ByVal statusMessage As String, ByRef validBins As BinsCollection)
If Not (validBins Is Nothing) Then
cboAssignBin.DataSource = validBins
cboAssignBin.DisplayMember = "BinNumber"
If validBins.Count > 0 Then
UpdateBinInfoLabels(DirectCast(cboAssignBin.SelectedItem, Bin))
End If
Busy = False
Windows.Forms.Cursor.Current = Cursors.Default
End If
End Sub
Finished! The work was done in another thread and the UI has been updated.
The entire form class looks like this:
Public Delegate Function FillBinsCallback() As BinsCollection
Public Delegate Sub UpdateUIHandler(ByVal statusMessage As String, ByRef validBins As BinsCollection)
Public Class BinMoveProduct
' delegate object to execute method asynchronously
Private BinFillerAsynch As FillBinsCallback = AddressOf BuildValidBinsCollection
' delegate object to service callback from CLR
Private CallbackHandler As AsyncCallback = AddressOf UpdateUI_Impl
#Region "PopulateBinsList"
Public Sub PopulateBinsList()
' calculate capacity
If (IncludeRecallsCheckBox.Checked) Then
_binCapacity = EnteredItem.WarehouseRecords(0).onHand + _
EnteredItem.WarehouseRecords(0).onOrder + _
EnteredItem.InRecall
Else
_binCapacity = EnteredItem.WarehouseRecords(0).onHand + _
EnteredItem.WarehouseRecords(0).onOrder
End If
' get valid bins
Windows.Forms.Cursor.Current = Cursors.WaitCursor
Busy = True
cboAssignBin.Text = "Working...Please wait"
' call BeginInvode on the BinFillerAsynch to create our collection of valid bins
BinFillerAsynch.BeginInvoke(AddressOf DatabindBinsList, BinFillerAsynch)
End Sub
Public Sub DatabindBinsList(ByVal ar As IAsyncResult)
Try
Dim ValidBins As BinsCollection
ValidBins = BinFillerAsynch.EndInvoke(ar)
UpdateUI("Complete", ValidBins)
Catch ex As Exception
Dim msg As String = "Error in DatabindBinsList: " & ex.Message.ToString
UpdateUI(msg, Nothing)
End Try
End Sub
Public Sub UpdateUI(ByVal statusMessage As String, ByRef validBins As BinsCollection)
' switch control back to the main thread (to update the UI
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {statusMessage, validBins}
' call BeginInvoke method of the form object
Me.BeginInvoke(handler, args)
End Sub
Public Sub UpdateUI_Impl(ByVal statusMessage As String, ByRef validBins As BinsCollection)
If Not (validBins Is Nothing) Then
cboAssignBin.DataSource = validBins
cboAssignBin.DisplayMember = "BinNumber"
If validBins.Count > 0 Then
UpdateBinInfoLabels(DirectCast(cboAssignBin.SelectedItem, Bin))
End If
Busy = False
Windows.Forms.Cursor.Current = Cursors.Default
End If
End Sub
Public Function BuildValidBinsCollection() As BinsCollection
BuildValidBinsCollection = _validBins.GetValidBins(EnteredItem.AlphaCfg, _
_warehouseNumber, _
_binCapacity)
End Function
#End Region
-- Matt Ranlett