To encourage users to run the worker component continuously, it uses a system tray interface. When the application is first started, it loads a system tray icon. Users can right-click the system tray icon to receive a menu with options for exiting the application or submitting new tasks, as shown in Figure 6-3. Another option would be to implement the worker as a Windows service that starts automatically when the computer boots up.
Creating a system tray application is quite easy. First, we create a Component class that holds the logic for the ContextMenu and NotifyIcon controls. All component classes have a design-time surface where you can create and store these objects, much like the component tray when designing a form. This allows you to configure the menu properties quickly using designers, rather than code it all manually in your startup class.
The skeleton for this class is shown here:
Public Class Startup Inherits System.ComponentModel.Component Friend WithEvents mnuContext As System.Windows.Forms.ContextMenu Friend WithEvents mnuShowStatus As System.Windows.Forms.MenuItem Friend WithEvents mnuSeparator As System.Windows.Forms.MenuItem Friend WithEvents mnuExit As System.Windows.Forms.MenuItem Friend WithEvents TrayIcon As System.Windows.Forms.NotifyIcon ' This is the object that provides the client-side remotable interface. Private Client As New ClientProcess() ' This is the main status form. We create it here to ensure that there's ' ever only one instance. Private frm As New MainForm() Public Sub New() frm.Client = Client InitializeComponent() End Sub Private Sub InitializeComponent() ' (Designer code omitted.) End Sub ' (Event handlers go here.) End Class
On startup, the code creates our component, ensures the NotifyIcon is visible, and logs in to the server through the remotable ClientProcess.
Public Shared Sub Main() Dim Startup As New Startup() Startup.TrayIcon.Visible = True ' Create the new remotable client object. Startup.Client.Login() ' Prevent the application from exiting prematurely. System.Windows.Forms.Application.Run() End Sub
The NotifyIcon has an attached context menu, which is immediately available. The menu items allow the user to exit the application or access the main window:
Private Sub mnuShowStatus_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles mnuShowStatus.Click frm.Show() End Sub Private Sub mnuExit_Click(ByVal sender As Object, ByVal e As System.EventArgs) _ Handles mnuExit.Click If Client.Status = BackgroundStatus.Processing Then MessageBox.Show("A background task is still in progress.", "Cannot Exit") Else Try Client.LogOut() Catch ' Ignore error that might occur if server no longer exists. End Try ' Clear the system tray icon manually. ' Otherwise, it may linger until the user moves the mouse over it. TrayIcon.Visible = False System.Windows.Forms.Application.Exit() End If End Sub
The ClientProcess class follows a similar model to the chat client in our earlier Talk .NET example. It calls server methods to request a new task and receives task-complete notifications or task requests. If it receives information that the main form needs to access, it raises an event. In addition, it includes two readonly properties, which provide the server-generated GUID and the current status (which indicates if the worker is currently carrying out a prime number search). The possible status values are provided in an enumeration:
Public Enum BackgroundStatus Processing Idle End Enum
Note that the ClientProcess class works both as a task worker (by implementing ITaskWorker) and as a TaskRequester (by implementing ITaskRequester). Here's the essential code, without the remotable methods:
Public Class ClientProcess Inherits MarshalByRefObject Implements ITaskWorker, ITaskRequester ' This event occurs when work begins or ends on the background thread. Public Event BackgroundStatusChanged(ByVal sender As Object, _ ByVal e As BackgroundStatusChanged) ' This event occurs when the prime number series is received ' (answer to a query). Public Event ResultsReceived(ByVal sender As Object, _ ByVal e As ResultsReceivedEventArgs) ' The reference to the server object. Private Server As ITaskServer ' The server-assigned ID. Private _ID As Guid Public ReadOnly Property ID() As Guid Get Return _ID End Get End Property ' Indicates whether prime number work is being carried out. Private _Status As BackgroundStatus = BackgroundStatus.Idle Public ReadOnly Property Status() As BackgroundStatus Get Return _Status End Get End Property Public Sub New() ' Configure the client channel for sending messages and receiving ' the server callback. RemotingConfiguration.Configure("TaskWorker.exe.config") ' Create the proxy that references the server object. Server = CType(Activator.GetObject(GetType(ITaskServer), _ "tcp://localhost:8000/WorkManager/TaskServer"), ITaskServer) End Sub Public Sub Login() ' Register the current worker with the server. _ID = Server.AddWorker(Me) End Sub Public Sub LogOut() Server.RemoveWorker(ID) End Sub ' This override ensures that if the object is idle for an extended ' period, it won't lose its lease and be garbage collected. Public Overrides Function InitializeLifetimeService() As Object Return Nothing End Function ' Submits client's request to the server. Public Sub FindPrimes(ByVal fromNumber As Integer, ByVal toNumber As Integer) Server.SubmitTask(New TaskRequest(Me, fromNumber, toNumber)) End Sub <System.Runtime.Remoting.Messaging.OneWay()> _ Public Sub ReceiveTask(ByVal task As TaskComponent.TaskSegment) _ Implements TaskComponent.ITaskWorker.ReceiveTask ' (Code omitted.) End Sub <System.Runtime.Remoting.Messaging.OneWay()> _ Public Sub ReceiveResults(ByVal results As TaskComponent.TaskResults) _ Implements TaskComponent.ITaskRequester.ReceiveResults ' (Code omitted.) End Sub End Class
The remotable ReceiveTask() and ReceiveResults() methods are both implemented as one-way methods so that the server won't be put on hold while the client deals with the information. The ReceiveTask() method performs all of its work directly in the method body, and then returns the completed segment to the server. An event is fired to notify the client form when the processing status changes.
_Status = BackgroundStatus.Processing ' Raise an event to alert the form that the background thread is processing. RaiseEvent BackgroundStatusChanged(Me, _ New BackgroundStatusChanged(BackgroundStatus.Processing)) ' Find the prime numbers and submit the list to the server. task.Primes = Erastothenes.FindPrimes(task.FromNumber, task.ToNumber) Server.ReceiveTaskComplete(task, ID) ' Raise an event to alert the form that the background thread is finished. _Status = BackgroundStatus.Idle RaiseEvent BackgroundStatusChanged(Me, _ New BackgroundStatusChanged(BackgroundStatus.Idle))
Alternatively, you could implement a separate thread to do this work, which would then call ReceiveTaskComplete() when finished. This would give the client the ability to cancel, prioritize, or otherwise monitor the thread as needed.
The ReceiveResults() method simply raises an event to the client with the list of primes:
' Raise an event to notify the form. RaiseEvent ResultsReceived(Me, New ResultsReceivedEventArgs(results.Primes))
Here's the code detailing the two custom EventArgs objects used by the ClientProcess:
Public Class ResultsReceivedEventArgs Inherits EventArgs Private _Primes() As Integer Public Property Primes() As Integer() Get Return _Primes End Get Set(ByVal Value As Integer()) _Primes = Value End Set End Property Public Sub New(ByVal primes() As Integer) _Primes = primes End Sub End Class Public Class BackgroundStatusChanged Inherits EventArgs Private _Status As BackgroundStatus Public Property Status() As BackgroundStatus Get Return _Status End Get Set(ByVal Value As BackgroundStatus) _Status = Value End Set End Property Public Sub New(ByVal status As BackgroundStatus) Me.Status = status End Sub End Class
The main form allows the user to submit new tasks and see if the local worker is currently occupied with a task segment. The form is shown in Figure 6-4.
The form code is quite straightforward. When the user clicks the Find Primes button, the start time is recorded and the ClientProcess.FindPrimes() method is called, which will forward the request to the server. If there's an error (for example, the server can't find any available workers), it will appear in the interface immediately.
Private StartTime As DateTime Private Sub cmdFind_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdFind.Click txtResults.Text = "" lblTimeTaken.Text = "" Try StartTime = DateTime.Now Client.FindPrimes(txtFrom.Text, txtTo.Text) Catch Err As Exception MessageBox.Show(Err.ToString()) End Try End Sub
The form handles both the BackgroundStatusChanged and the ResultsReceived events, and updates the interface accordingly. However, before the update is performed, the code must be marshaled to the correct userinterface thread. To accomplish this goal, we reuse the UpdateControlText object introduced in the last chapter.
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
Private Sub Client_BackgroundStatusChanged(ByVal sender As Object, _ ByVal e As TaskWorker.BackgroundStatusChanged) _ Handles Client.BackgroundStatusChanged Dim NewText As String If e.Status = BackgroundStatus.Idle Then NewText = "The background thread has finished processing its " & _ "prime number query, and is now idle." ElseIf e.Status = BackgroundStatus.Processing Then NewText = "The background thread has received a new " & _ "prime number query, and is now processing it." End If Dim ThreadsafeUpdate As New UpdateControlText(NewText, lblBackgroundInfo) ' Invoke the update on the user-interface thread. Me.Invoke(New MethodInvoker(AddressOf ThreadsafeUpdate.Update)) End Sub
When results are received, the array of prime numbers is converted to a long string, which is used to fill a text box. A StringBuilder object is used to quickly build up the string. This operation is much faster than string concatenation, and the difference is dramatic. If you run the same code without using a StringBuilder, you'll notice that the Time Taken label is updated long before the prime number list appears.
Private Sub Client_ResultsReceived(ByVal sender As Object, _ ByVal e As TaskWorker.ResultsReceivedEventArgs) Handles Client.ResultsReceived Dim NewText As String NewText = DateTime.Now.Subtract(StartTime).ToString() Dim ThreadsafeUpdate As New UpdateControlText(NewText, lblTimeTaken) ' Invoke the update on the user-interface thread. Me.Invoke(New MethodInvoker(AddressOf ThreadsafeUpdate.Update)) Dim Builder As New System.Text.StringBuilder() Dim Prime As Integer For Each Prime In e.Primes Builder.Append(Prime.ToString() & " ") Next NewText = Builder.ToString() ThreadsafeUpdate = New UpdateControlText(NewText, txtResults) ' Invoke the update on the user-interface thread. Me.Invoke(New MethodInvoker(AddressOf ThreadsafeUpdate.Update)) End Sub
There are a couple of additional form details that aren't shown here. For example, if the user attempts to close the form, you need to make sure that it isn't disposed, only hidden. You can see all the details in the code download provided for this chapter.
Figure 6-5 shows a prime number query that was satisfied by multiple clients.
Figure 6-6 shows the server log for the operation.
If you run multiple instances of the TaskWorker on the same computer, you'll be able to test the system, but the processing speed won't increase. That's because all workers are still competing for the resources of the same computer.