JavaScript Editor JavaScript Editor     JavaScript Debugger 



Team LiB
Previous Section Next Section

Creating the Task Worker

The peer application performs two functions: It allows a user to submit task requests, and it performs prime number calculations when instructed to by the server.

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.


Figure 6-3: The worker in the system tray

The System Tray Interface

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

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

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.

Click To expand
Figure 6-4: The main form

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

When the background status changes, a label control is modified accordingly:

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.

Click To expand
Figure 6-5: A completed prime number query

Figure 6-6 shows the server log for the operation.

Click To expand
Figure 6-6: The server trace transcript
Tip 

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.


Team LiB
Previous Section Next Section


JavaScript Editor Free JavaScript Editor     JavaScript Editor


R7