Wait for Task to Complete without Blocking UI Thread

MoonKnight picture MoonKnight · Aug 23, 2015 · Viewed 7.8k times · Source

I have a fairly complex WPF application that (much like VS2013) has IDocuments and ITools docked within the main shell of the application. One of these Tools needs to be shutdown safely when the main Window is closed to avoid getting into a "bad" state. So I use Caliburn Micro's public override void CanClose(Action<bool> callback) method to perform some database updates etc. The problem I have is all of the update code in this method uses MongoDB Driver 2.0 and this stuff is async. Some code; currently I am attempting to perform

public override void CanClose(Action<bool> callback)
{
    if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
    {
        using (ManualResetEventSlim tareDownCompleted = new ManualResetEventSlim(false))
        {
            // Update running test.
            Task.Run(async () =>
                {
                    StatusMessage = "Stopping running backtest...";
                    await SaveBackTestEventsAsync(SelectedBackTest);
                    Log.Trace(String.Format(
                        "Shutdown requested: saved backtest \"{0}\" with events",
                        SelectedBackTest.Name));

                    this.source = new CancellationTokenSource();
                    this.token = this.source.Token;
                    var filter = Builders<BsonDocument>.Filter.Eq(
                        BackTestFields.ID, DocIdSerializer.Write(SelectedBackTest.Id));
                    var update = Builders<BsonDocument>.Update.Set(BackTestFields.STATUS, TestStatus.Cancelled);
                    IMongoDatabase database = client.GetDatabase(Constants.DatabaseMappings[Database.Backtests]);
                    await MongoDataService.UpdateAsync<BsonDocument>(
                        database, Constants.Backtests, filter, update, token);
                    Log.Trace(String.Format(
                        "Shutdown requested: updated backtest \"{0}\" status to \"Cancelled\"",
                        SelectedBackTest.Name));
                }).ContinueWith(ant =>
                    {
                        StatusMessage = "Disposing backtest engine...";
                        if (engine != null)
                            engine.Dispose();
                        Log.Trace("Shutdown requested: disposed backtest engine successfully");
                        callback(true);
                        tareDownCompleted.Set();
                    });
            tareDownCompleted.Wait();
        }
    }
}

Now, to start with I did not have the ManualResetEventSlim and this would obviously return to the CanClose caller before I updated my database on the background [thread-pool] thread. In an attempt to prevent the return until I have finished my updates I tried to block the return, but this freezes the UI thread and prevents anything from happening.

How can I get my clean-up code to run without returning to the caller too early?

Thank for your time.


Note, I cannot override the OnClose method using async signature as the calling code would not await it (I have no control over this).

Answer

Julien Lebot picture Julien Lebot · Aug 23, 2015

I don't think you have much choice than to block the return. However your updates should still run despite the UI thread being locked. I wouldn't use a ManualResetEventSlim, but just a simple wait() and a single task without a continuation. The reason for that is by default Task.Run prevents the child task (your continuation) from being attached to the parent and so your continuation may not have time to complete before the window closes, see this post.

public override void CanClose(Action<bool> callback)
{
    if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
    {
        // Update running test.
        var cleanupTask = Task.Run(async () =>
        {
            StatusMessage = "Stopping running backtest...";
            await SaveBackTestEventsAsync(SelectedBackTest);

            // other cleanup  tasks
            // No continuation

            StatusMessage = "Disposing backtest engine...";
             if (engine != null)
                engine.Dispose();
             Log.Trace("Shutdown requested: disposed backtest engine successfully");
             callback(true);
        });
        cleanupTask.Wait();
    }
}

You can also use TaskFactory.StartNew with TaskCreationOptions.AttachedToParent if you really need to use a continuation.