Daniel Peñalba
Daniel Peñalba

Reputation: 31857

How to respond events while a threaded operation is running in .NET

I have a class to launch background operations in a WinForms application. I need to write this background worker since my requisites are using .NET 1.1, so I cannot use BackgroundWorker, that is only available from .NET 2.0

This class get a delegate and execute it in a thread. I want the main thread to respond to events.

I also want to indicate that the operation is running setting the application cursor to Cursors.WaitCursor.

What do you think about current implementation? I'm interested in the method WaitTillThreadFinishes(), because I'm not sure about Application.DoEvents(), please read the code and share with me opinions about WaitTillThreadFinishes.

The following code executes the operation:

private object ExecuteOperation (Delegate target, params object[] parameters)
{
    mTargetDelegate = target;
    mTargetParameters = parameters;

    mTargetThread = new Thread(new ThreadStart(ThreadProc));
    mTargetThread.Name = mTargetDelegate.Method.Name;

    mOperationFinished = false;

    // start threaded operation
    mTargetThread.Start();

    // perform active waiting
    WaitTillThreadFinishes();

    return mTargetResult;
 }

The following code is executed in a thread, simply call the delegate, and wrap exceptions:

protected virtual void ThreadProc()
{
    try
    {
        mTargetResult = mTargetDelegate.DynamicInvoke(mTargetParameters);
    }
    catch (ThreadAbortException) { }
    catch (Exception ex)
    {
        //manage exceptions here ...
    }
    finally
    {
        mOperationFinished = true;
    }
}

And this is the code performs an active waiting. I'm interested on share with you. Any better option? Any pain calling Application.DoEvents() massively?

private void WaitTillThreadFinishes ()
{
    // Active wait to respond to events with a WaitCursor
    while (!mOperationFinished)
    {
        // sleep to avoid CPU usage
        System.Threading.Thread.Sleep(100);
        Application.DoEvents();
        Cursor.Current = Cursors.WaitCursor;
    }
    Cursor.Current = Cursors.Default;
}

Thanks in advance.

Upvotes: 2

Views: 728

Answers (4)

user1228
user1228

Reputation:

There isn't much support in 1.1 for doing this, but I'll tell you what I'd do (sorry, no code at this time).

As for the asynchronous operation, I'd use the APM to kick off and complete the asynchronous method. This is fully supported in 1.1, so no worries there.

The idea is that in the UI, you store some indication that work is being done (a boolean field, for example) and (optionally) a Timer used to "wake up" the UI on a regular basis to check on the current status of the background work and indicate this to the user.

You would set the boolean to indicate you are working in the background, call BeginInvoke() on your delegate (using the overload that takes a callback search for "Executing a Callback Method When an Asynchronous Call Completes "), and start the Timer. When the user attempts to use the UI, you would optionally check the boolean and cancel the operation, thus preventing the user from doing something harmful while you are waiting. When the timer Ticks, you can check the status of your asynchronous method by, say, a shared field that the method writes updates to and the UI reads. For example, a double which the UI uses to update a progress bar.

Once the callback fires, you clean up your asynchronous mess (i.e., call EndInvoke, and handle any exceptions thrown, etc), turn off the Timer and reset your boolean running indication field.

By using this method, you can keep the UI completely responsive (and partially usable, depending on your overall design), can set up a mechanism to abort the background worker (through the use of another field, the reverse of the boolean mentioned earlier, and inform the user of the status of the operation.

Upvotes: 1

nick2083
nick2083

Reputation: 1973

Please let me know if i understood your question correctly. Why dont you use an event to notify the UI that the worker finished his job? This way, the UI doen't get blocked by the worker, and you avoid busy waiting.

Sample Implementation

public class MyBackgroundWorker
{
    // Fields
    private Delegate _target;
    private object[] _arguments;

    // Events
    public event EventHandler RunWorkerStarted;
    public event EventHandler<RunWorkerCompletedEventArgs> RunWorkerCompleted;

   // Event Invocators
    public void InvokeRunWorkerStarted()
    {
        var handler = RunWorkerStarted;
        if (handler != null) handler(this, new EventArgs());
    }

    public void InvokeRunWorkerCompleted(object result)
    {
        var handler = RunWorkerCompleted;
        if (handler != null) handler(this, new RunWorkerCompletedEventArgs(result));
    }

    public void RunWorkerAsync(Delegate target, params object[] arguments)
    {
        _target = target;
        _arguments = arguments;

        new Thread(DoWork).Start(arguments);
    }

    // Helper method to run the target delegate
    private void DoWork(object obj)
    {
        _target.DynamicInvoke(_arguments);
        // Retrieve the target delegate's result and invoke the RunWorkerCompleted event with it (for simplicity, I'm sending null)
        InvokeRunWorkerCompleted(null);
    }
}

internal class RunWorkerCompletedEventArgs : EventArgs
{
    public RunWorkerCompletedEventArgs(object result)
    {
        Result = result;
    }

    public object Result { get; set; }
}

Usage

In the UI you can use it this way:

    private void button1_Click(object sender, EventArgs e)
    {
        var worker = new MyBackgroundWorker();
        worker.RunWorkerStarted += worker_RunWorkerStarted;
        worker.RunWorkerCompleted += worker_Completed;
        worker.RunWorkerAsync(new MethodInvoker(SomeLengthyOperation), null);
    }

    void worker_RunWorkerStarted(object sender, EventArgs e)
    {

    }

    void worker_Completed(object sender, EventArgs e)
    {
        MessageBox.Show("Worker completed");
    }

    private void SomeLengthyOperation()
    {
       Thread.Sleep(5000);
    }

Final Notes

Remember to Invoke() in the event handlers to access the UI thread correctly. You can also modify the worker so this is done in a safe way.

Upvotes: 3

Ian Boyd
Ian Boyd

Reputation: 256731

Ideally you start the asynchronous operation and leave your form alone (aside from maybe using the Cursors.AppStarting cursor).

When your threaded operation completes, it then needs to fire some sort of BackgroundOperationComplete event. This is where your would call from your asynchronous delegate code:

form.Invoke(BackgroundOperationComplete);

The form's BackgroundOperationComplete method is where you can handle the fact that the background operation is complete:

void BackgroundOperationComplete()
{
    this.Cursor = Cursors.DefaultCursor;

    lblAnswer.Text = "The thread is done";
}

If all else fails, keep the operation synchronous, and use an IProgressDialog. (brief conceptual pseudo-code from memory):

void DoStuff()
{
   IProgressDialog pd = new ProgressDialog();
   pd.SetTitle = "Calculating Widgets";
   pd.StartTimer(PDTIMER_RESET, NULL)
   pd.StartProgressDialog(this.Handle, NULL,  PROGDLG_MODAL | PROGDLG_NOTIME | PROGDLG_NOPROGRESSBAR | PROGDLG_NOCANCEL, NULL);
   try
   {
      pd.SetLine(1, "Please wait while the widgets are frobbed");

      DoTheThingThatDoesTheSynchronousStuff();
   }
   finally
   {
      pd.StopProgressDialog();
   }
   pd = null;
}

Upvotes: -1

Schroedingers Cat
Schroedingers Cat

Reputation: 3129

There is occasionally a case for kicking off a thread and waiting for its return, if you are doing other things in the meantime, but in this case, with the code you have shown, it is meaningless.

If you want the threadProc to allow for events to be processed, then call doevents in that, which will free up the CPU briefly, allowing for processing.

Unless you have a particular reason for needing to thread processes, you should not do it. Getting it right - as Ian Boyd has said - is difficult, and the more you need to interact with it the harder it is. If you can run fire-and-forget threads, that is the easiest.

Upvotes: 0

Related Questions