Wait for Task to Complete without Blocking UI Thread

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.


You can use something similar to WinForm's Application.DoEvents but for WPF, it involves using a flag, firing your task, not Waiting for it, but continiously processing UI messages in a loop until your task is done and sets the flag. e.g.:

if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running))
{
    bool done = 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));
        StatusMessage = "Disposing backtest engine...";
        if (engine != null)
            engine.Dispose();
        Log.Trace("Shutdown requested: disposed backtest engine successfully");
        callback(true);
        done = true;
    });

    while (!done)
    {
        Application.Current.Dispatcher.Invoke(DispatcherPriority.Background,
                                new Action(delegate { }));
    }
}

It's a bit hacky, but given your situation and no control over the calling code, it might be your only option to maintain a responsive UI without immediately returning to the caller.