Atlanta .NET Regular Guys

Community Blog for two guys in Atlanta that focus on Microsoft and Community.

Quick About

This is the community blog for Brendon Schwartz and Matt Ranlett.  If you want to see their technical posts visit http://www.sharepointguys.com

Back To DevCow

Recent Posts

Tags

Email Notifications

    Archives

    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

    Posted: 01-13-2006 1:39 PM by Matt Ranlett | with 3 comment(s)
    Filed under:

    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