Wait for the task to complete without blocking the UI thread.

I have a rather complicated WPF application that (like VS2013) has IDocuments and ITools fixed in the main shell of the application. One of these Tools should be safely disabled when the main window is closed to avoid getting into a “bad” state. Therefore, I use the Caliburn Micro public override void CanClose(Action<bool> callback) to perform some database updates, etc. The problem I have is all the update code in this method using MongoDB Driver 2.0, and this async stuff. Some code; I'm currently trying to execute

 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 the fact that I did not have ManualResetEventSlim , and this will obviously return to the calling CanClose before I update my database in the background thread [pool-pool]. In an attempt to prevent the return until I finish my updates, I tried to block the return, but this freezes the UI thread and prevents anything.

How can I run my clearing code without returning to the caller too soon?

Thank you for your time.


Please note: I cannot override the OnClose method using asynchronous signature, because the call code does not wait for it (I can not control this).

+8
c # asynchronous semaphore task blocking
source share
3 answers

I do not think you have a choice but to block the return. However, your updates should still be performed despite blocking the UI thread. I would not use ManualResetEventSlim, but just just wait () and one task without continuing. The reason for this is, by default, Task.Run does not allow the child task (your continuation) to be bound to the parent object, and therefore your continuation may not have time to finish before the window closes, see this message .

 public override void CanClose(Action<bool> callback) { if (BackTestCollection.Any(bt => bt.TestStatus == TestStatus.Running)) { // Update running test. var cleanupTask = Task.Run(async () => { Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Stopping running backtest..."; })); await SaveBackTestEventsAsync(SelectedBackTest); // other cleanup tasks // No continuation Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Disposing backtest engine..."; })); if (engine != null) engine.Dispose(); Log.Trace("Shutdown requested: disposed backtest engine successfully"); callback(true); }); while (!cleanupTask.IsCompleted) { Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { })); } } } 

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

EDIT: I combined my answer with @Saeb Amini, this is a bit of a hack overall, but you keep the interface responsive.

EDIT 2: Here's a sample demonstration of a solution (tested with a new WPF project):

 public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } protected override void OnClosing(CancelEventArgs e) { var dispatcher = Application.Current.Dispatcher; var cleanupTask = Task.Run( async () => { dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate {StatusMessage.Text = "Stopping running backtest..."; })); await Task.Delay(2000); dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { StatusMessage.Text = "Disposing backtest engine..."; })); await Task.Delay(2000); }); while (!cleanupTask.IsCompleted) { dispatcher.Invoke(DispatcherPriority.Background, new Action(delegate { })); } } } 
+7
source share

You can use something similar to WinForm Application.DoEvents , but for WPF it involves using a flag, launching your task, not Wait for it, but constantly process user interface messages in a loop until your task is completed, and set the flag. eg:.

 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 { })); } } 

A bit hacky, but considering your situation and not controlling the calling code, this may be your only option to maintain a flexible user interface without immediately returning to the caller.

+4
source share

I tried the async / wait combination to solve this problem. First we convert CanClose synchronization to async void. The async void method then calls the async Task method to do this work. We must do this because there is a danger of asynchrony 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 advantages to using this approach:

  • easier to follow and understand
  • simplified exception handling

Note:

  • I missed the cancellation token in the code snippet, which can be easily added if you want.
  • async / await keywords exist after .NET Framework 4.5 and C # 5.0
0
source share

All Articles