Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when a Task is running and it's window is closed?

Tags:

c#

.net-4.0

In my WPF Window_Loaded event handler I have something like this:

System.Threading.Tasks.Task.Factory.StartNew(() =>
    {
        // load data from database

        this.Dispatcher.Invoke((Action)delegate
        {
            // update UI with loaded data
        });
    });

What I want to know is what happens when the user closes the form while data is being loaded from the database and before the this.Dispatcher.Invoke routine is ran?
Will there be an ObjectDisposedException thrown? Or will the Dispatcher ignore the Invoke routine (as the window is disposed)?

I've tried to figure this out my self with some basic testing, but my results so far have been that nothing bad happens. No exceptions are thrown and the application doesn't crash.

And yet I have had a couple of bad experiences before, when I used the ThreadPool for essentialy the same thing. In the Window_Loaded event handler I queued a new user work item into the ThreadPool, while the data was being loaded I pressed the Esc key (I had a Window_KeyUp event handler listen for the Esc key and if it was pressed it called this.Close()) and the application crashed when it tried to update the UI (inside Dispatcher.Invoke), since the window was already closed.

Because the Task library uses the ThreadPool behind the scenes, I'm afraid that this might happen again unless I write code to protect my application...

Let's change the scenario a bit - what happens when the user closes the form while the UI is being updated in the Dispatcher.Invoke routine?
Will the form closing be postponed until the Invoke method has returned? Or could some exception be thrown?

If there is a possibility for exceptions to be thrown, how best to handle them?

The best that I can come up with, is to have a bool readyToClose = false; Wire up a Window_Closing event handler which checks if (!readyToClose) e.Cancel = true; Then after I have updated my UI I can set readyToClose = true, thus if the user tries to close the form too soon, it will be canceled.

Or should I instead use try { ... } catch (ObjectDisposedException) { // do nothing } ?

like image 401
Marko Avatar asked Apr 06 '11 07:04

Marko


2 Answers

In Windows Forms, calling Invoke on some control (such as your main form) will indeed throw if that control is disposed (e.g. the user closed the form). A simple way to avoid it would be using the winforms SynchronizationContext class. This works because the WindowsFormsSynchronizationContext keeps an internal control of its own, on which it calls the Invoke/BeginInvoke commands. see Exploiting the BackGroundWorker for cross-thread invocation of GUI actions on Winforms controls?

In WPF, the SynchronizationContext delegates to the dispatcher so using either of them is the same. However, since the WPF dispatcher is not disposable as controls are (it can be shut down though), you don't have to worry about an ObjectDisposedException. However, I believe calling Invoke may hang your application if the dispatcher has been shut down (since it would wait for an operation that would never complete) - calling BeginInvoke instead should take care of that. That being said, ThreadPool threads (which are the ones created by the default Task scheduler, as in your case above) are Background threads which would not stop your process from exiting - even if they are hung on a Dispatcher.Invoke call.

In short, in both Windows Forms and WPF, using SynchronizationContext.Post (which in WPF is equivalent to Dispatcher.BeginInvoke) will take care of the general problem you're talking about.

Let's change the scenario a bit - what happens when the user closes the form while the UI is being updated in the Dispatcher.Invoke routine? That can't happen - while the Dispatcher.Invoke invoke is running the UI thread is busy, in particular it can't process user input such as the keyboard or mouse click a user would need to close the form.

like image 63
Ohad Schneider Avatar answered Oct 13 '22 16:10

Ohad Schneider


I had a long running task, called OfflineExportTask, as a property of the window:

private Task OfflineExportTask { get; set; }

I also had a cancellation token source property in the window:

private CancellationTokenSource _cts;
private CancellationTokenSource Cts =>
    _cts ?? (_cts = new CancellationTokenSource());

The task started when the application opened if it had been more than 7 days since the last export. It called an ExportForOfflineMode method in a class called MemberService:

try
{
    OfflineExportTask = Task.Factory.StartNew(() => 
        MemberService.ExportForOfflineMode(Cts.Token),
        Cts.Token, TaskCreationOptions.LongRunning,    
        TaskScheduler.Default);

    // The task must be awaited in order to catch any exceptions.
    await OfflineExportTask;
}
catch (OperationCanceledException)
{
    // Do nothing. The application is closing while the task 
    // was still in progress.
}
catch (Exception ex)
{
    // Regular exception handling code
}

Having the task cancel when the window closed was tricky. Comments in the code below explain:

private void Window_Closing(object sender, CancelEventArgs e)
{
    #region Comments

    // In case an offline export is still unfinished, cancel its Task.     
    // Otherwise an exception is raised for terminating it improperly.
    //
    // This is tricky because: 
    // * The task cancellation happens when the window is being closed, 
    //   i.e., when this method is called.
    // * Closure of the window must be prevented until OfflineExportTask has 
    //   completed its cancellation.
    // * After its cancellation is complete, then this window must close, 
    //   which terminates the application.
    // 
    // To accomplish this behavior:
    // * OfflineExportTask is tested to see if it is still in progress when 
    //   this method is called:
    //   > If OfflineExportTask is null, an offline export was never 
    //     started.
    //   > If OfflineExportTask's status is RanToCompletion, it's done so 
    //     doesn't need to be cancelled.
    //   > If OfflineExportTask's status is Canceled, it doesn't need to be     
    //     cancelled again. See the explanation below for this condition.
    // * If OfflineExportTask needs to be cancelled:
    //   > It is cancelled by calling Cts.Cancel()
    //   > The closure of the window is prevented with the code 
    //     e.Cancel = true
    //   > This method is exited.
    // * To get the window to close once cancellation is complete:
    //   > Waiting for cancellation is accomplished by using ContinueWith, 
    //     which starts a new task the moment OfflineExportTask is complete 
    //     (i.e., its cancellation is complete).
    //   > The new ContinueWith task executes only one line of code: Close()
    //   > The ContinueWith statement specifies 
    //     TaskScheduler.FromCurrentSynchronizationContext(), which is 
    //     necessary since the Close() call must happen on the GUI thread.
    //   > Executing ContinueWith's Close() code causes this method, 
    //     Window_Closing, to be called a second time. 
    //   > When this method is called the second time, OfflineExportTask's 
    //     status is TaskStatus.Canceled, which is why the "if" statement 
    //     tests for this condition.
    //   > When this method is called the second time, none of the code 
    //     within the "if" block is executed and the window closes normally.
    // 
    // Note that the following code was attempted, which specifies 
    // ContinueWith with the starting of the OfflineExportTask:
    // 
    // OfflineExportTask = Task.Factory.StartNew(()=>
    //    MemberService.ExportForOfflineMode(Cts.Token),
    //    Cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default)
    //    .ContinueWith(t => Close(), CancellationToken.None, 
    //    TaskContinuationOptions.OnlyOnCanceled,
    //    TaskScheduler.FromCurrentSynchronizationContext());
    // 
    // This did not work for this scenario, as OfflineTaskExport's status is 
    // still Running the second time this Window_Closing method gets called. 
    // Additionally, even when this was tested for, the window still didn't     
    // close.

    #endregion

    if (OfflineExportTask != null
        && OfflineExportTask.Status != TaskStatus.RanToCompletion
        && OfflineExportTask.Status != TaskStatus.Canceled
        && OfflineExportTask.Status != TaskStatus.Faulted)
    {
        // Establish task that will run the moment the OfflineExportTask's 
        // cancellation is complete. All it does is close the application, 
        // i.e., call this method again.
        OfflineExportTask.ContinueWith((antecedent) => Close(),
            TaskScheduler.FromCurrentSynchronizationContext());

        // Cancel the OfflineExportTask.
        Cts.Cancel();

        // Prevent the window from closing.
        e.Cancel = true;

        // BusyIndicator is a Telerik WPF control, not germane to this
        // topic. Serves as an example of how a progress indicator can
        // be used.
        BusyIndicator.BusyContent = "Canceling export. Please wait... ";
        BusyIndicator.IsBusy = true;

        return;
    }

    // This code is an example of something that should be executed only
    // when the window is actually closing.
    // Save the window's current position and size to restore these settings 
    // the next time the application runs. 
    Settings.Default.StartLeft = Left;
    Settings.Default.StartTop = Top;
    Settings.Default.StartWidth = Width;
    Settings.Default.StartHeight = Height;
    Settings.Default.Save();
}
like image 39
CMarsden Avatar answered Oct 13 '22 16:10

CMarsden