8

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 } ?

Marko
  • 2,266
  • 4
  • 27
  • 48

2 Answers2

4

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.

Community
  • 1
  • 1
Ohad Schneider
  • 36,600
  • 15
  • 168
  • 198
  • Do I understand correctly, that a call to "this.Dispatcher" inside my Task will never throw an ObjectDisposedException? But what about all the UI objects I'm referencing inside the Invoke routine? Finally as per your statement - "Dispatcher.BeginInvoke) will take care of the general problem you're talking about" - would I not have had my application crash when using the ThreadPool directly (as described in my post) if I had used BeginInvoke instead of Invoke? – Marko Apr 06 '11 at 18:51
  • There is only one UI thread (the dispatcher thread), so while you're inside the invoke routine the user can't close the form. He can only do so just before the task runs, in which case your invoke routine would never run (since the Dispatcher message loop won't be active). In terms of crashing/hanging, there's no difference between using the threadpool and using tasks since the latter are implemented in terms of the former (so yes, I believe BeginInvoke won't crash with the threadpool as well) – Ohad Schneider Apr 06 '11 at 20:46
  • In any case, I recommend you debug and check whether your WPF elements are "shut down" when you think they are: http://social.msdn.microsoft.com/Forums/en/wpf/thread/3bb34dcf-433e-4ddd-8b6e-13b83f075eed – Ohad Schneider Apr 06 '11 at 20:51
  • What I meant by "that a call to this.Dispatcher", was referencing "this" and then the "Dispatcher" property. I was thinking that calling "this" once the window had been closed, could pose problems. Can I safely assume, that because in WPF I don't manually dispose of the window (like in WinForms), the GC can see that I still have code referencing "this" and won't dispose of it just yet? – Marko Apr 07 '11 at 06:15
  • About "all the UI objects I'm referencing inside Invoke routine" - well, my debugging shows that if I close the window during data loading, then the Invoke routine is still executed. So your statement "in which case your invoke routine would never run" should be false... – Marko Apr 07 '11 at 06:16
  • "I was thinking that calling "this" once the window had been closed, could pose problems" - You may be right, Reflector shows that there are indeed cases where `this.Dispatcher` would be null - so simply use the static `Dispatcher.CurrentDispatcher` and you won't have to worry about it – Ohad Schneider Apr 07 '11 at 19:08
  • "well, my debugging shows that if I close the window during data loading, then the Invoke routine is still executed" - That's very strange, try adding the "IsDisposed" check mentioned in the MSDN forum link I supplied above - but do so *inside* the invoke routine – Ohad Schneider Apr 07 '11 at 19:11
  • I should perhaps mention that the window I'm talking about is a secondary one. The main window keeps running all the time and thus the application is alive. So on that basis, the Dispatcher itself will never be shutdown... What I would like to ask is if there might be any changes between .NET 3.5 SP1 and .NET 4 that might influence the ThreadPool scenario? I tried to replicate the error I had before with ThreadPool, but couldn't do it. I can't currently try to debug in .NET 3.5, because of a VS2010 bug, but I do know that the only thing that has changed is the framework version... – Marko Apr 08 '11 at 16:39
  • I'll mark your post as the answer and use all the info you have provided. If I can't reproduce any errors while debuging (which I have had in the past in this scenario) and take all the extra precautions (`Dispatcher.CurrentDispatcher + BeginInvoke + IsDisposed`) then hopefully everything will work in the production enviroment as well. – Marko Apr 08 '11 at 16:45
  • 1) A call to Dispatcher.CurrentDispatcher is pointless, because it looks for the Dispatcher in the current thread. Inorded to get the UI Dispatcher I'd have to cache the result before starting my Task. Since I don't win anything by this, I may aswell simply call this.Dispatcher. 2) The final post in the MSDN link you provided has a fundamental flaw in it. As per MSDN documentation, a call to PresentationSource.FromVisual() will return null if the visual is disposed. So instead of `PresentationSource.FromVisual(this).IsDisposed` I have to use `PresentationSource.FromVisual(this) != null`. – Marko Apr 09 '11 at 08:53
  • 1) My mistake, use `Application.Current.Dispatcher` (since `this.Dispatcher` could still possibly be null, maybe) 2) In the same thread they also mention `return new WindowInteropHelper(window).Handle == IntPtr.Zero;` that would work if you simply wanted to know whether a window was closed or not – Ohad Schneider Apr 09 '11 at 18:27
  • 1) Can you perhaps elaborate a bit where you found that `this.Dispatcher` can be null? I tried looking in reflector, but couldn't spot any such cases... I'd be interested to know the possible cases when this.Dispatcher could return null. 2) `PresentationSource.FromVisual(this) != null` seems to be working very well and is a bit clearer to read then your other suggestion so I'll stick with that. PS! Thanks for your answer and comments, much appreciated. – Marko Apr 09 '11 at 22:10
  • My pleasure :) Regarding the nullity issue, `DispatcherObject` has a method called `DetachFromDispatcher` which nulls `this.Dispatcher`. It appears only the following WPF elements call it, though: `Freezable`, `Media`, `ResourceDictionary`, `Style`, `StyleHelper`. It's an internal method, so nothing else should be able to call it (without reflection, that is). So it appears `this.Dispatcher` would be safe for any control not in the above list. Come to think of it, `Application.Current` is itself a `DispatcherObject`, having the same `DetachFromDispatcher` - but it's not in the list :) – Ohad Schneider Apr 10 '11 at 15:47
2

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();
}
CMarsden
  • 361
  • 3
  • 5