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.
Tip |
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 Try Dim DestinationPath As String = "C:\TEMP\" & e.Filename ' Receive the file. TalkClient.AcceptFile(e.SenderAlias, e.FileIdentifier, _ DestinationPath) ' 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.