Wednesday, July 16, 2008

Visual Studio Threading – Part III

This is a part of a series of articles on my experiences with Visual Studio Threading and Advantage Events.

In the part three of this series I will discuss my experience using a BackgroundWorker. This turned out to be the best way to work with Advantage Events from a .NET application, if you are unfamiliar with Advantage Events take a look at this Tech Tip.

This class uses a ThreadPool to reduce the overhead when it is instantiated. It also has the ability to interact with the UI through the use of events, not to be confused with Advantage Events. The background worker can signal the main thread with the RunWorkerCompleted event or with a ProgressChanged event. A function can be defined which runs on the main thread in response to these events. The BackgroundWorker class can also support cancellation which allows them to be stopped when necessary.

Overall this approach was the easiest to implement since the BackgroundWorker class was specifically designed to run an asyncrhonous process which could still interact with the UI. To implement this I first had to create a background worker and specify some options.

   1: static BackgroundWorker bwRoomStatus = new BackgroundWorker();
   2:  
   3: // Setup BackgroundWorker properties
   4: bwRoomStatus.WorkerSupportsCancellation = true;
   5: bwRoomStatus.WorkerReportsProgress = true;
   6:  
   7: // Setup BackgroundWorker events
   8: bwRoomStatus.DoWork += bwRoomStatus_DoWork;
   9: bwRoomStatus.ProgressChanged += bwRoomStatus_ProgressChanged;
  10: bwRoomStatus.RunWorkerCompleted += bwRoomStatus_RunWorkerCompleted;

The DoWork event opens its own connection to Advantage and creates an even. Events are identified by the connection string and must be created for each connection that wants to be notified of the event. The next step is to run the sp_WaitForEvent system procedure which will notify the client if the event has been signaled. If the event has been signaled the ProgressChanged event is fired which notifies the main application thread.

   1: static void bwRoomStatus_DoWork(object sender, DoWorkEventArgs e)
   2: {
   3:     AdsConnection cn;
   4:     AdsCommand cmd;
   5:     AdsDataReader dr;
   6:  
   7:     try
   8:     {
   9:         // Connect using the same path as the application
  10:         cn = new AdsConnection(sConnection);
  11:         cn.Open();
  12:         cmd = cn.CreateCommand();
  13:  
  14:         // Create the RoomStatus event for this connection
  15:         cmd.CommandText = "EXECUTE PROCEDURE sp_CreateEvent('RoomStatus', 0)";
  16:         cmd.ExecuteNonQuery();
  17:     }
  18:     catch (AdsException aex)
  19:     {
  20:         if (aex.Number == 5051)
  21:         {
  22:             // Event already exists so continue
  23:         }
  24:         else
  25:         {
  26:             e.Result = aex;
  27:             return;
  28:         }               
  29:     }
  30:     
  31:     while (!e.Cancel)
  32:     {
  33:         // Wait for the RoomStatus event for up to 5 seconds
  34:         cmd.CommandText = "EXECUTE PROCEDURE sp_WaitForEvent('RoomStatus', 5000, 0, 0)";
  35:  
  36:         dr = cmd.ExecuteReader();
  37:  
  38:         // check to see if the room status was updated
  39:         dr.Read();
  40:         if (dr.GetInt32(1) > 0) 
  41:         {
  42:             bwRoomStatus.ReportProgress(100);
  43:         }
  44:         dr.Close();
  45:  
  46:         // Check to see if the worker has been canceled
  47:         if (bwRoomStatus.CancellationPending)
  48:             e.Cancel = true;
  49:     }
  50:     
  51:     // Close the connection
  52:     cn.Close();
  53: }

The ProgressChanged event is run by the main thread whenever it is signaled by my BackgroundWorker this allows it to update the GUI. In this case it updates a ListView control to reflect the latest changes.

   1: private void bwRoomStatus_ProgressChanged(object sender, ProgressChangedEventArgs e)
   2: {
   3:     lblThreadStatus.Text = "Updated: " + DateTime.Now.ToString("HH:mm:ss");
   4:     cmAds = cnAds.CreateCommand();
   5:     cmAds.CommandText = "Select * from Room";
   6:     drStatus = cmAds.ExecuteReader();
   7:     PopulateListView(drStatus);
   8: }

The final event we have to handle is the RunWorkerCompleted. This event is run by the main thread when the BackgroundWorker exits either because it is done or it has been canceled. In the example the GUI is updated to reflect that monitoring of room status is turned off.

   1: private void bwRoomStatus_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
   2: {
   3:     DateTime dtUpdate = new DateTime();
   4:  
   5:     if (e.Cancelled)
   6:     {
   7:         lblThreadStatus.Text = "Room Status Canceled";
   8:     }
   9:     else if (e.Error != null)
  10:     {
  11:         // An error has occured
  12:         lblThreadStatus.Text = "Error Unable to get Room Status";
  13:     }
  14:     else
  15:     {
  16:         // Report the status this should not happen
  17:         lblThreadStatus.Text = e.Result.ToString();
  18:     }
  19:  
  20:     // Clear the room status list
  21:     lvRoomStatus.Items.Clear();
  22: }

Event Application Screenshot

Now that all of the events have been handled I have to start the BackgroundWorker using the RunWorkerAsync method which runs the DoWork event and begins waiting for notifications from the server. Now any application which makes changes to the data, using the same connection path as the application, will signal an event.

My example (coming soon) application uses the idea of a doctor’s office which has several exam rooms. The application shows the status of each of the exam rooms using a color code. As the status in the exam rooms is changed the room status is updated automatically on the application, see the example to the right (click for a larger view)

There are many other ways to use threads with .NET but the BackgroundWorker has many benefits. It has been designed to provide an easy way to create a background process and alert the main thread when it is complete. This works perfectly for waiting for Advantage Events. You can even use the same BackgroundWorker to listen for multiple events using the sp_WaitForAnyEvent system procedure. You could identify the event that was fired by passing different integer values for ProgressChanged.

No comments: