Welcome to Atlanta .NET Regular Guys Sign in | Join | Help

Adventures in Delegates part 2

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

Published 13-01-2006 01:39 by Matt Ranlett
Filed Under:

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Jim Wooley said:

Great writeup. I'm glad to see the content juices running again.

An alternative to the above, you could use the BackgroundWorker component in 2.0. Ken Getz had a good set of articles on this for MSDN Mag last year. See http://msdn.microsoft.com/msdnmag/issues/05/03/AdvancedBasics/.
January 13, 2006 1:36 PM
 

Keith Rome said:

Sweet Mary, Mother of Jesus!

Matt is writing advanced .NET code?!
January 13, 2006 2:07 PM
 

Atlanta .NET Regular Guys said:

I want to be a good boy, I really do!
Consider this scenario for me:  I've got a SQL query which...
January 19, 2006 1:39 PM

What do you think?

(required) 
(optional)
(required) 

About Matt Ranlett

One of the two original Atlanta .NET Regular Guys, Matt fills his free time by helping to run several Atlanta area user groups, the Atlanta Code Camps, and works as one of the two INETA co-Vice Presidents of Technology
SkinName:iroha_Blog2
Powered by Community Server, by Telligent Systems