Recipe 9.2 Canceling an Asynchronous Query

Recipe 9.2 Canceling an Asynchronous Query

Problem

Given a query running that runs asynchronously on a background thread, you want to give the user the option to cancel the query if it is taking too long.

Solution

Abort the background thread and clean up in an exception handler.

The sample code contains two event handlers and a single method:

Start Button.Click

Checks whether there is an existing background thread loading the DataSet . If the DataSet is not being loaded, a new thread is created invoking the AsyncFillDataSet( ) method to fill a DataSet . Otherwise, a message is displayed stating that the DataSet is currently being filled.

Cancel Button.Click

Aborts the background thread filling the DataSet .

AsyncFillDataSet( )

This method loads a DataSet with the Orders and Order Details tables from the Northwind database. The method displays a message when the method has started and when it has completed. The method also traps the ThreadAbortException to handle the situation where the fill on the background thread is canceled .

The C# code is shown in Example 9-2.

Example 9-2. File: AsynchronousFillCancelForm.cs
 // Namespaces, variables, and constants
using System;
using System.Configuration;
using System.Threading;
using System.Data;
using System.Data.SqlClient;

// Table name constants
private const String ORDERS_TABLE       = "Orders";
private const String ORDERDETAILS_TABLE = "OrderDetails";

// Relation name constants
private const String ORDERS_ORDERDETAILS_RELATION =
    "Orders_OrderDetails_Relation";

// Field name constants
private const String ORDERID_FIELD      = "OrderID";
private const String ORDERDATE_FIELD    = "OrderDate";

private Thread thread;

//  . . . 

private void startButton_Click(object sender, System.EventArgs e)
{
    // Check if a new thread can be created.
    if (thread == null 
        (thread.ThreadState & (ThreadState.Unstarted 
        ThreadState.Background)) == 0)
    {
        // Create and start a new thread to fill the DataSet.
        thread = new Thread(new ThreadStart(AsyncFillDataSet));
        thread.IsBackground = true;
        thread.Start( );
    }
    else
    {
        // DataSet already being filled. Display a message.
        statusTextBox.Text += "DataSet still filling  . . . " +
            Environment.NewLine;
        statusTextBox.Refresh( );
    }
}

private void cancelButton_Click(object sender, System.EventArgs e)
{
    // Check if the thread is running and an abort has not been requested.
    if (thread != null &&
        (thread.ThreadState &
        (ThreadState.Stopped  ThreadState.Aborted 
        ThreadState.Unstarted  ThreadState.AbortRequested)) == 0)
    {
        try
        {
            // Abort the thread.
            statusTextBox.Text += "Stopping thread  . . . " +
                Environment.NewLine;
            statusTextBox.Refresh( );
            thread.Abort( );
            thread.Join( );
            statusTextBox.Text += "Thread stopped." +
                Environment.NewLine;
        }
        catch (Exception ex)
        {
            statusTextBox.Text += ex.Message + Environment.NewLine;
        }
    }
    else
    {
        statusTextBox.Text += "Nothing to stop." + Environment.NewLine;
    }
}

private void AsyncFillDataSet( )
{
    try
    {
        statusTextBox.Text = "Filling DataSet  . . . " +
            Environment.NewLine;
        statusTextBox.Refresh( );

        DataSet ds = new DataSet("Source");
        
        SqlDataAdapter da;

        // Fill the Order table and add it to the DataSet.
        da = new SqlDataAdapter("SELECT * FROM Orders",
            ConfigurationSettings.AppSettings["Sql_ConnectString"]);
        DataTable orderTable = new DataTable(ORDERS_TABLE);
            da.FillSchema(orderTable, SchemaType.Source);
        da.Fill(orderTable);
        ds.Tables.Add(orderTable);

        // Fill the OrderDetails table and add it to the DataSet.
        da = new SqlDataAdapter("SELECT * FROM [Order Details]",
            ConfigurationSettings.AppSettings["Sql_ConnectString"]);
        DataTable orderDetailTable = new DataTable(ORDERDETAILS_TABLE);
        da.FillSchema(orderDetailTable, SchemaType.Source);
        da.Fill(orderDetailTable);
        ds.Tables.Add(orderDetailTable);

        // Create a relation between the tables.
        ds.Relations.Add(ORDERS_ORDERDETAILS_RELATION,
            ds.Tables[ORDERS_TABLE].Columns[ORDERID_FIELD],
            ds.Tables[ORDERDETAILS_TABLE].Columns[ORDERID_FIELD],
            true);

        statusTextBox.Text += "DataSet fill complete." +
            Environment.NewLine;
    }
    catch (ThreadAbortException ex)
    {
        // Exception indicating that thread has been aborted
        statusTextBox.Text += "AsyncFillDataSet( ): " + ex.Message +
            Environment.NewLine;
    }
} 

Discussion

Recipe 9.1 discusses using a background thread to fill a DataSet to improve application performance.

The ThreadState of a thread specifies its execution state. This value is a bitwise combination of ThreadState enumeration described in Table 9-1.

Table 9-1. ThreadState enumeration

Value

Description

Aborted

The thread is stopped.

AbortRequested

The Abort( ) method of the thread has been called but the thread has not yet received the ThreadAbortException that will terminate it.

Background

The thread is being executed on a background thread rather than a foreground thread. This is specified by the IsBackground property of the thread.

Running

The thread has been started, is not blocked, and there is no pending ThreadAbortException .

Stopped

The thread is stopped.

StopRequested

The thread is being requested to stop. This value is for internal use only.

Suspended

The thread is suspended.

SuspendRequested

The thread is being requested to suspend.

Unstarted

The thread is not started and the Start( ) method has not been called on the thread.

WaitSleepJoin

The thread is blocked by a Wait( ) , Sleep( ) , or Join( ) method.

In the solution, a background thread is used to fill the DataSet . The ThreadState of the thread object is used to determine whether it can be started or whether it can be aborted, as follows :

  • A thread can be started only if it does not have a ThreadState of Unstarted or Background .

  • A thread can be aborted only if its ThreadState is not Stopped , Aborted , Unstarted , or AbortRequested .

The Abort( ) method of the Thread raises a ThreadAbortException in the thread on which it is invoked and begins the process of terminating the thread. ThreadAbortException is a special exception, although it can be caught, it is automatically raised again at the end of a catch block. All finally blocks are executed before killing the thread. Because the thread can do an unbounded computation in the finally blocks, the Join( ) method of the threada blocking call that does not return until the thread actually stops executingis used to guarantee that the thread has terminated . Once the thread is stopped, it cannot be restarted.