10

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).

MoonKnight
  • 23,214
  • 40
  • 145
  • 277
  • Shouldn't the caller make the decision to close the window based on the callback and not whether you returned from the method ? I would think that if you spun your task and returned immediately, the window shouldn't be closed until you do callback(true/false). – Julien Lebot Aug 23 '15 at 14:30
  • Nah, unfortunately not. If I don't set the call back Caliburn assumes that is was `callback(true)` which "says" this component can be closed. If I put in `callback(false)` ofcourse the close operation is cancelled altogether. – MoonKnight Aug 23 '15 at 14:32
  • I could write my own close behaviour class but I think I should be able to do what I want here without going overboard... – MoonKnight Aug 23 '15 at 14:33

3 Answers3

7

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.

Saeb Amini
  • 23,054
  • 9
  • 78
  • 76
Julien Lebot
  • 3,092
  • 20
  • 32
  • Yeah, simple and nice. Not sure why I couldn't see this... I will have a go at implementing this now and feed back with an accept. Thanks very much for your time. – MoonKnight Aug 23 '15 at 14:56
  • The only issue I am finding with this is that my `NotifyOnPropertyChanged` delegates used when getting the Selected test, updating the `StatusMessage` thread are blocked because I am waiting on the UI thread. I need to pulse this until I have finished with my work, but how in this situation? – MoonKnight Aug 23 '15 at 15:06
  • Ah I assumed you weren't doing any UI updates in this method, I'm updating the answer – Julien Lebot Aug 23 '15 at 15:11
  • I am looking at using `TaskScheduler.FromCurrentSynchronizationContext()` to force the thread to run on the GUI's context. – MoonKnight Aug 23 '15 at 15:11
  • @Killercam What am I missing? What is the use of creating a task and then *Wait* it. What is the difference than running all the stuff on the calling thread? – Eser Aug 23 '15 at 15:35
  • `await` I can't change Mongos await API. It I don't use await the calls will be non-blocking and the function will return, Caliburn assuming everything was okay then closes the application. I need to be able to await calls but also access properties that are bound to the UI. It is all far from ideal, but this is the position in which I find myself... – MoonKnight Aug 23 '15 at 15:44
  • @Killercam And what about using `Wait` or `Result` instead of *awaiting* tasks (like `SaveBackTestEventsAsync(SelectedBackTest).Wait()`)? – Eser Aug 23 '15 at 15:49
  • 1
    @Eser true but wouldn't he loose the UI responsiveness ? I mean while the database is doing its thing, the UI might "go blank" and Windows might warn the user that his application is no longer responsive. When using a task and manually pumping the message thread he can still update the UI while doing his database updates. – Julien Lebot Aug 23 '15 at 16:02
5

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.

Saeb Amini
  • 23,054
  • 9
  • 78
  • 76
  • The while clause is especially interesting for those that are not familiar with how SynchronizationContext works... – Fazi Aug 04 '16 at 12:33
0

I tried the async/await combination to resolve this kind of problem. First we convert the sync void CanClose to async void. Then the async void method calls the async Task method to do the work. We have to do this because the danger of async void when catching exceptions.

public override async void CanClose(Action<bool> callback)
{
   await CanCloseAsync(callback);
}

public async Task CanCloseAsync(Action<bool> callback)
{
    var result1 = await DoTask1();
    if (result1)
        await DoTask2();
    callback(result1);
}

In my opinion, there are benefits of using this approach:

  • easier to follow and understand
  • easier exception handling

Note:

  • I omitted the cancellation token in the code snippet, which can be added easily if you want to.
  • async/await keywords exist after .net framework 4.5 and c# 5.0
ganagus
  • 54
  • 2