Returning Values from Other Threads

In the previous example, you most likely thought it odd, or at least inefficient, to have to notify the user when the thread is done, and then to have the user click a button that loads the data into the grid. A seemingly obvious answer would be to have the GetData sub return an ADO.NET dataset when it is done, and then have the calling procedure fill the grid.

There are two problems with this approach, however. First, because you are running the GetData sub on a separate thread, the calling routine continues executing immediately after starting the new thread. That means any code you had to fill the grid after calling GetData would execute immediately, even though GetData would most likely not be finished. Obviously, this is not a good approach.

The second problem with adding a return value to the GetData sub is that you can't return a value from a different thread. When you are calling procedures or methods on another thread, those procedures or methods cannot return a value.

Given these two issues—the second of which is an intentional limitation—how would your client application know that the GetData method has finished and then be able to fill in the grid automatically? There are two approaches: You can set a global variable, or you can fire an event from the method on the thread.

Returning Data Using Global Variables

The current example uses global variables, in the sense that the DataSet is a module-level variable, and the GetData sub assigns the return value of the XML Web service to it, even though GetData is running on a separate thread. However, your program doesn't know when the value is actually created, so you'll have to add some code to the client to figure this out.

When you use global variables, you can use either of two ways to determine when the thread has finished. First, you could check from the client application to see whether the thread still exists. Second, you could have the application running on a separate thread fire an event, which you could handle in the client. You'll read more about handling the event in the next section, because an event can be used to pass data back to the calling program, which can eliminate the need for having to use global variables.

To see how to use global variables and check the status of the thread, you will use the IsAlive property of the Thread class. IsAlive returns a Boolean indicating the status of the thread. IsAlive returns True as long as the thread is started and running; after the thread dies, IsAlive returns False.

To check the IsAlive property and wait for the thread to die, you need to have either a loop that constantly checks to see whether IsAlive is False, or you need to have a timer check periodically. Neither of these approaches is really ideal because both continuously run code to check for conditions, when an event would serve the same purpose without chewing up processor cycles to check a status.

To see how you could check the IsAlive property, modify your cmdGetData_Click sub to contain the following code:

Private Sub cmdGetData_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles cmdGetData.Click
   Dim threadGetData As New System.Threading.Thread(AddressOf GetData)
   threadGetData.Start()
   Do While threadGetData.IsAlive
      Application.DoEvents()
   Loop
   DataGrid1.DataSource = myDS
   'the DataMember will be whatever table you saw
   'when you created the dataset
   DataGrid1.DataMember = "Customers"
End Sub

In this code, you start the thread, and then enter a loop that runs as long as IsAlive is True. Notice that inside the loop, however, is an Application.DoEvents(). This is necessary to free up the resources for the main thread to actually see the change in the status of threadGetData's IsAlive property. Without this DoEvents, your code will never see IsAlive change to False.

Before you run the example, you'll want to remove the message box from the GetData procedure because its presence would require you to clear the box in order for the thread to die. Reduce your GetData procedure to just the following code:

Private Sub GetData()
   Dim myOrders As New Orders()
   myDS = myOrders.GetOrders
End Sub

Run the application. After the loop is finished, your code will set the DataSource and DataMember properties of the DataGrid, and the grid will be populated automatically when the thread is finished. This makes the application more intelligent, but it isn't the most efficient method. The first problem is that most programming purists frown upon the use of global or module-level variables. That argument aside, the fact that you have a Do...Loop means that you are consuming processor cycles the entire time the loop is running.

Using the Join Method

There is an alternative to checking the IsAlive property: The Join method of the Thread class waits until a thread dies. Actually, the Join method is overloaded. The first implementation waits until the thread dies. The second and third implementations wait either for the thread to die or for a specified time to lapse, with the only difference being how you specify the time.

You can see how the Join method works by modifying your code to look like this:

Private Sub cmdGetData_Click(ByVal sender As System.Object, _
 ByVal e As System.EventArgs) Handles cmdGetData.Click
   Dim threadGetData As New System.Threading.Thread(AddressOf GetData)
   threadGetData.Start()
   threadGetData.Join()
   DataGrid1.DataSource = myDS
   'the DataMember will be whatever table you saw
   'when you created the dataset
   DataGrid1.DataMember = "Customers"
End Sub

If you run this code, however, you'll notice one important facet of the Join method: It blocks other execution on the main thread while it waits for threadGetData to die. You can verify this by clicking the Get Data button and then clicking the Beep button: nothing happens. This means that as long as Join is monitoring the thread, nothing will happen on the thread handling the user interface. Obviously, this is not what is desired in this case.

Don't think that Join is not useful, however. Sometimes, you'll want one thread to wait for another thread to finish. For example, if one thread is getting the data, you might want a second thread to wait for the data to come back so that it can operate on that data. In this case, Join is much cleaner than looping constantly and checking the IsAlive property.

Returning Data Using Events

Many people prefer to raise events to return data from one thread to the parent thread. Unfortunately, this requires some changes to the current application. XML Web services are powerful, but they aren't really designed to raise events in the client program. Therefore, you need to create a simple class that raises the event for you. The good news is that this class can wrap around the call to the Orders Web service. So, you will still retrieve data with the Web service, but you'll notify the client that the thread is done by raising an event, and you'll pass the data back the same way.

In the project, add a new class and name it ServiceWrapper.vb. Inside the class, add the following code:

Public Class ServiceWrapper
   Public Event DataReturn(ByVal OrderDS As System.Data.DataSet)

   Public Sub GetData()
      Dim myDS As New System.Data.DataSet()
      Dim myOrders As New Orders()
      myDS = myOrders.GetOrders
      RaiseEvent DataReturn(myDS)
   End Sub
End Class

In ServiceWrapper, you define an event named DataReturn, which returns an ADO.NET DataSet object when it is raised. Then you define a method called GetData. GetData does in the wrapper class what you had been doing in the client: It instantiates an instance of the Orders Web service and calls GetOrders. The return is an ADO.NET DataSet, which you then pass to the DataReturn when you call the RaiseEvent function.

Raising the event is great, but your client has to handle the event. In the client, you can get rid of the Load Grid button if you want. Make modifications to your code so that your final code looks like this (minus the “Windows Form Designer generated code” section, of course):

Public Class Form1
   Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "
   Dim WithEvents SW As New ServiceWrapper()

   Private Sub cmdGetData_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles cmdGetData.Click
      AddHandler SW.DataReturn, AddressOf DataReturnedEH
      Dim threadGetData As New System.Threading.Thread(AddressOf SW.GetData)
      threadGetData.Start()
   End Sub

   Sub DataReturnedEH(ByVal ReturnedDS As System.Data.DataSet)
      DataGrid1.DataSource = ReturnedDS
   End Sub

   Private Sub cmdBeep_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles cmdBeep.Click
      Beep()
   End Sub
End Class

The first new thing that you might notice is the declaration for the ServiceWrapper object. Because you are declaring the ServiceWrapper object WithEvents, you must declare it at the module level. Next, inside the cmdGetData_Click method, you use the AddHandler function to define a procedure that will handle the DataReturn event from the ServiceWrapper. Then you kick the ServiceWrapper's GetData method off on its own thread.

As you know, the GetData method in ServiceWrapper calls the Web service and then returns an ADO.NET DataSet using the DataReturn event. Your event handler, named DataReturnEH, handles the event and receives the dataset that is passed as a parameter. Inside the event handler, you set the DataGrid's DataSource property to this returned dataset.

You might notice that the DataGrid's DataMember property is not being set. Actually, Windows controls are not the best examples to use for multithreading because there are a number of issues with them, the main one of which is that many methods of controls are not thread-safe, which means you cannot call them from any thread except the thread on which they are running. For this example, leave things as they are and run the project. The grid will show a small plus sign in the upper-left corner when the dataset is back. If you expand the tree view by clicking the plus sign, you will see a list of the DataTable objects in the dataset. Only one table is listed, and if you click on that, you will see the data in the grid. You can see this in Figure 11.2.

Figure 11.2. The grid displaying a list of available DataTables in the DataSet.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.111.33