The Internal OrderFill ClientThe internal client provides the features needed to process orders after they've been submitted. It also has the ability to dynamically receive a message and update the interface accordingly, thanks to some careful threading code. Figure 17-9 shows the interface that appears when the internal client is started. Figure 17-9. The OrderFill client at startup
The first step is to register the current queue and retrieve the IDs for all the available orders. The database connection and queue path are drawn from the configuration file shown here: <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="TransactConnection" value="Data Source=localhost;Initial Catalog=Transact;Integrated Security=SSPI" /> <add key="MessageQueue" value="FARIAMAT\private$\TransactNotify" /> </appSettings> </configuration> Listing 17-21 shows the code that runs when the internal client starts. It registers the queue, retrieves the order IDs, and starts a separate thread that monitors the queue for new messages. (We'll deal with the threading code that's involved a little later.) Listing 17-21 Configuring the internal client on startup' The remote OrderService. Private Proxy As New localhost.OrderService() ' The local database component. Private DB As New DBComponent.TransactTables() ' The custom threaded object that monitors the queue. Private WithEvents Monitor As New QueueMonitor() ' The thread where the QueueMonitor performs its work. Private MonitorThread As New Thread(AddressOf Monitor.DoWatch) Private Sub OrderFillClient_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Register queue. Proxy.RegisterNotifyQueue( _ ConfigurationSettings.AppSettings("MessageQueue")) Dim ID As Integer For Each ID In DB.Order.GetAvailableOrderIDs() lstAvailable.Items.Add(ID) Next ' Start the queue monitor. MonitorThread.Priority = ThreadPriority.BelowNormal MonitorThread.Start() End Sub The two arrow buttons enable users to place orders in progress or return them to the available pool. The client can place multiple orders in progress at the same time, which can be useful as different teams in the production center work to assemble the parts for different orders. Often, while one order is underway but waiting (for example, while special software is being installed), another order is filled. When an order is checked out, a full structure is returned with all the order information. This object is placed directly in the in-progress list control (as shown in Listing 17-22). The list control calls the object's ToString method to determine what text it should display. In the case of the Order object, this method is overridden to return some useful information, including the order ID and the total number of stations. Listing 17-22 Putting an order in progressPrivate Sub cmdCheckOut_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdCheckOut.Click ' Ensure an item is currently selected. If lstAvailable.SelectedIndex <> -1 Then Dim Ord As Order Try ' Put the record in progress. ' This step allows you to run multiple OrderFillClient ' instances at the same time without conflict. Ord = DB.Order.SetOrderInProgress(Val(lstAvailable.Text)) ' Move the item from one listbox to the next. ' Note that the item in lstInProgress is a full-fledged ' Order object, with all the order information. lstInProgress.Items.Add(Ord) lstAvailable.Items.Remove(lstAvailable.SelectedItem) Catch Err As Exception MessageBox.Show(Err.ToString, "Error") End Try End If End Sub Whenever an item is selected from the in-progress list control, the corresponding order information is displayed (as shown in Figure 17-10). Listing 17-23 shows the event-handling code that displays the information. Listing 17-23 Displaying order informationPrivate Sub lstInProgress_SelectedIndexChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles lstInProgress.SelectedIndexChanged If lstInProgress.SelectedIndex <> -1 Then Dim Ord As Order = CType(lstInProgress.SelectedItem, Order) Dim Display As String Display = "Order ID: " & Ord.OrderID.ToString() Display &= Environment.NewLine Display &= "Customer ID: " & Ord.CustomerID & _ Display &= Environment.NewLine Dim Station As Station, Item As OrderItem For Each Station In Ord.Stations Display &= Environment.NewLine Display &= " **** STATION ****" & Environment.NewLine For Each Item In Station.OrderItems Display &= " * ITEM * " Display &= Item.ProductName & ": " Display &= Item.PriceOrderAt.ToString("C") Display &= Environment.NewLine Next Next txtOrderInfo.Text = Display Else ' This automatically clears the order display when the list ' is emptied. txtOrderInfo.Text = "" End If End Sub Figure 17-10. Displaying information for an in-progress order
Orders can be returned to the pool just as easily as they are retrieved, as shown in Listing 17-24. Listing 17-24 Returning an in-progress orderPrivate Sub cmdReject_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdReject.Click If lstInProgress.SelectedIndex <> -1 Then Try Dim ID As Integer ID = CType(lstInProgress.SelectedItem, Order).OrderID DB.Order.SetOrderAvailable(ID) ' Move the item from one listbox to the next. lstAvailable.Items.Add(ID) lstInProgress.Items.Remove(lstInProgress.SelectedItem) Catch Err As Exception MessageBox.Show(Err.ToString, "Error") End Try End If End Sub To complete an order, the ID is retrieved from the currently selected item and submitted to the database component (as shown in Listing 17-25). The unique ID for the shipping record is displayed in a message box so it can be noted in the production paperwork. Listing 17-25 Completing an orderPrivate Sub cmdComplete_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdComplete.Click Try ' Complete the order. Dim OrderID, ShipID As Integer OrderID = CType(lstInProgress.SelectedItem, Order).OrderID ShipID = DB.Order.CompleteOrder(OrderID) ' Update the list. lstInProgress.Items.Remove(lstInProgress.SelectedItem) ' Notify the user. MessageBox.Show("Confirmation for this shipment is: " & _ ShipID.ToString, "Shipment Recorded") Catch MessageBox.Show(Err.ToString, "Error") End Try End Sub Note that error-handling code is always used when the application performs a database operation. Otherwise, an unhandled error could cause the application to end and jeopardize any other orders that are currently in progress. When the application closes, it automatically returns all in-progress orders and terminates the queue-monitoring thread (as shown in Listing 17-26). It also unregisters the queue to inform the XML Web service that notification messages are no longer required. Listing 17-26 Returning orders on shutdownPrivate Sub OrderFillClient_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles MyBase.Closing ' Unregister the queue. Proxy.RegisterNotifyQueue("") ' You must use this method to reject all the currently in-progress ' orders. Otherwise, they will not be available in the future. Dim Ord As Order For Each Ord In lstInProgress.Items DB.Order.SetOrderAvailable(Ord.OrderID) Next ' It is safe to abort this thread while it is sleeping ' or waiting for messages. MonitorThread.Abort() MonitorThread.Join() End Sub The Queue MonitorThe final ingredient in the order-filling client is the custom queue-monitoring class. It follows the threading guidelines set out in Chapter 6. The process can be divided into five steps:
Listing 17-27 shows the custom object for monitoring the queue. The actual monitoring is performed in the DoWatch method. It loops continuously (so long as the RequestStop flag is not set). Each time it attempts to retrieve a message for 30 seconds. If no message is found, an exception is thrown and the thread is temporarily suspended. Listing 17-27 Monitoring the queuePublic Class QueueMonitor Public Event MessageReceived(ByVal sender As Object, _ ByVal e As MessageEventArgs) Public RequestStop As Boolean = False Public Sub DoWatch() Dim Queue As New MessageQueue( _ ConfigurationSettings.AppSettings("MessageQueue")) Dim Message As System.Messaging.Message Do Until RequestStop Dim AllowedTypes() As Type = {GetType(Integer)} Queue.Formatter = New XmlMessageFormatter(AllowedTypes) Try Message = Queue.Receive(TimeSpan.FromSeconds(30)) If Not Message Is Nothing Then RaiseEvent MessageReceived(Me, _ New MessageEventArgs(Message.Body)) End If Catch e As MessageQueueException ' No message was received after 30 seconds. If Not RequestStop Then Thread.Sleep(TimeSpan.FromSeconds(30)) End If End Try Loop End Sub End Class You could use the MessageQueue.Receive method without specifying a timeout, but this would tie up the thread indefinitely. By using the looping approach, the thread can still respond (within no more than 30 seconds) if the RequestStop variable is set to True. Incidentally, the code performs equally well if you loop more aggressively, reducing the timeout and thread sleep to 5 or fewer seconds. However, A. Datum considers a notification time of 30 seconds to be satisfactory. Optionally, you can retrieve the wait time from a configuration file and set it programmatically. When the event is fired, the update takes two steps. First, the event is handled and the message information is recorded. Second, the AddNewFromQueue method is invoked to update the user interface (as shown in Listing 17-28). Listing 17-28 Handling the MessageReceived eventPrivate Messages As New ArrayList() Private Sub Monitor_MessageReceived(ByVal sender As Object, _ ByVal e As MessageEventArgs) Handles Monitor.MessageReceived SyncLock Messages Messages.Add(e.Message) End SyncLock Me.Invoke(New MethodInvoker(AddressOf AddNewFromQueue)) End Sub Remember that the MessageReceived event takes places on the QueueMonitor thread, so it can't update the user interface directly. Instead, the Control.Invoke method marshals this code to the user interface thread, where it can perform its work safely. The private AddNewFromQueue function then updates the interface (as shown in Listing 17-29). Listing 17-29 Updating the interfacePrivate Sub AddNewFromQueue() SyncLock Messages Dim Message As String For Each Message In Messages lstAvailable.Items.Add(Message) Next Messages.Clear() End SyncLock End Sub No additional order information is required. The full order is retrieved if the order is selected and placed in progress. |