JavaScript Editor JavaScript Editor     JavaScript Debugger 

Team LiB
Previous Section Next Section

Threading and the Client

The most important place for threading code is at the coordination server, because it will regularly deal with simultaneous client requests. That's why the last few sections have dealt exhaustively with server-side threading issues. However, the peers in the system also expose an object through Remoting (called ClientProcess). That means that each client also has a pool of threads—provided by the CLR—listening for remote method calls. The ClientProcess object will be invoked on a different thread from the rest of the application, and multiple requests from different peers could be received at once.

What's worse, the code commits one of the cardinal sins of Windows programming: manipulating the user interface from a thread that doesn't own it. To deal with this reality and prevent another level of subtle, maddening bugs, you need to fortify the client and add some synchronization code.


To verify that the event handlers for events such as MessageReceived and FileOfferReceived don't execute on the user-interface thread, you can perform a simple test. Display the unique numeric identifier for the current thread (Thread.CurrentThread.Hashcode), either by showing a MessageBox or writing a debug statement.You'll see that the identifier for it isn't the same in the event handler as it is in other form methods.

Unfortunately, you can't lock user interface elements (such as controls). Instead, you need to ensure that code that interacts with the user interface executes on the user-interface thread. The base .NET Control class provides an Invoke() method designed for exactly this purpose. In order to execute a method on the user-interface thread, pass a reference to this method to the Invoke() method, using the MethodInvoker delegate.

MyControl.Invoke(New MethodInvoker(AddressOf MyMethod))

The MethodInvoker delegate can point to any method that takes no parameters. This means you need to perform a little bit more work if you want the method to have access to one or more variables. For example, in TalkClient, the method must have access to a string variable with the message text in it. The easy way to allow this is to create a dedicated class that combines the method with the required information. Here's the class used in the revised TalkClient:

Public Class UpdateControlText

    Private NewText As String

    ' The reference is retained as a generic control,
    ' allowing this helper class to be reused in other scenarios.
    Private ControlToUpdate As Control

    Public Sub New(ByVal newText As String, ByVal controlToUpdate As Control)
        Me.NewText = newText
        Me.ControlToUpdate = controlToUpdate
    End Sub

    ' This method must execute on the user-interface thread.
    Public Sub Update()
        Me.ControlToUpdate.Text &= NewText
    End Sub

End Class

As you can see, some effort has been made to ensure that this class is as generic as possible. It can be used to update the Text property of any control in a thread-safe manner. Here's how you'll put it to work when receiving a message:

Private Sub TalkClient_MessageReceived(ByVal sender As Object, _
  ByVal e As MessageReceivedEventArgs) Handles TalkClient.MessageReceived
    ' Define the text.
    Dim NewText As String
    NewText = "Message From: " & e.SenderAlias
    NewText &= " delivered at " & DateTime.Now.ToShortTimeString()
    NewText &= Environment.NewLine & e.Message
    NewText &= Environment.NewLine & Environment.NewLine

    ' Create the object.
    Dim ThreadsafeUpdate As New UpdateControlText(NewText, txtReceived)

    ' Invoke the update on the user-interface thread.
    Me.Invoke(New MethodInvoker(AddressOf ThreadsafeUpdate.Update))

End Sub

Ideally, all methods that access the user interface should be performed on the user-interface thread. That means you'll need to update the code that prompts the user to accept a file transfer in response to the FileOfferReceived method. Here's one option:

Private Sub TalkClient_FileOfferReceived(ByVal sender As Object, _
  ByVal e As TalkClient.FileOfferReceivedEventArgs) _
  Handles TalkClient.FileOfferReceived

    ' Create the user message describing the file offer.
    Dim Message As String
    Message = e.SenderAlias & " has offered to transmit the file named: "
    Message &= e.Filename & Environment.NewLine
    Message &= Environment.NewLine & "Do You Accept?"

      'Fortunately the MessageBox.Show method is thread-safe,
      'saving some work.
    Dim Result As DialogResult = MessageBox.Show(Message, _
      "File Transfer Offered", MessageBoxButtons.YesNo, MessageBoxIcon.Question)

    If Result = DialogResult.Yes Then

            Dim DestinationPath As String = "C:\TEMP\" & e.Filename

            ' Receive the file.
            TalkClient.AcceptFile(e.SenderAlias, e.FileIdentifier, _
          ' Display information about it in the chat window.
            Dim NewText As String
            NewText = "File From: " & e.SenderAlias
            NewText &= " transferred at " & DateTime.Now.ToShortTimeString()
            NewText &= Environment.NewLine & DestinationPath
            NewText &= Environment.NewLine & Environment.NewLine

          Dim ThreadsafeUpdate As New UpdateControlText(NewText, txtReceived)
          Me.Invoke(New MethodInvoker(AddressOf ThreadsafeUpdate.Update))

        Catch err As Exception
            MessageBox.Show(err.Message, "Transfer Failed", _
                             MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        End Try
    End If

End Sub

You won't need to take any extra steps when updating the user list—this call is performed on the user-interface thread thanks to a UI-friendly timer. This is the key difference between the System.Windows.Forms.Timer class and other classes in the System.Timers namespace.

Team LiB
Previous Section Next Section

JavaScript Editor Free JavaScript Editor     JavaScript Editor