2015-08-23 8 views
8

Ich habe eine ziemlich komplexe WPF-Anwendung, die (ähnlich wie VS2013) IDocuments und ITools im Hauptgehäuse der Anwendung angedockt hat. Eine dieser Tools muss sicher heruntergefahren werden, wenn das Hauptfenster geschlossen ist, um zu vermeiden, in einen "schlechten" Zustand zu kommen. Also verwende ich Caliburn Micro public override void CanClose(Action<bool> callback) Methode, um einige Datenbank-Updates etc. Das Problem, das ich habe, ist der gesamte Update-Code in dieser Methode verwendet MongoDB Driver 2.0 und das Zeug ist async. Irgendein Code; momentan bin ich versuche,Warten, bis Task abgeschlossen ist, ohne UI-Thread zu blockieren

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

Jetzt ausführen, um mit zu beginnen ich nicht die ManualResetEventSlim habe und dies den CanClose Anrufer offensichtlich zurückkehren würde, bevor ich meine Datenbank auf dem Hintergrund [Thread-Pool] Thread aktualisiert. In einem Versuch, die Rückkehr zu verhindern, bis ich meine Updates beendet habe, habe ich versucht, die Rückkehr zu blockieren, aber das friert den UI-Faden ein und verhindert, dass etwas geschieht.

Wie kann ich meinen Bereinigungscode ausführen, ohne zu früh zum Anrufer zurückzukehren?

Vielen Dank für Ihre Zeit.


Hinweis, ich kann die OnClose Methode Asynchron-Signatur nicht außer Kraft setzen, wie der aufrufende Code es nicht erwarten würde (ich habe keine Kontrolle über diese).

+0

Sollte der Anrufer nicht die Entscheidung treffen, um das Fenster basierend auf dem Rückruf zu schließen und nicht, ob Sie von der Methode zurück? Ich würde denken, dass, wenn Sie Ihre Aufgabe drehten und sofort zurückkehrten, das Fenster nicht geschlossen werden sollte, bis Sie Rückruf (wahr/falsch) machen. –

+0

Nein, leider nicht. Wenn ich den Callback nicht zurücksetze, nimmt Caliburn an, dass es "Callback (true)" ist, was "sagt", dass diese Komponente geschlossen werden kann. Wenn ich 'callback (false)' natürlich einfüge, wird der Close-Vorgang komplett abgebrochen. – MoonKnight

+0

Ich könnte meine eigene enge Verhaltensklasse schreiben, aber ich denke, ich sollte in der Lage sein, das zu tun, was ich will, ohne über Bord zu gehen ... – MoonKnight

Antwort

7

Ich glaube nicht, dass Sie viel Wahl haben, als die Rückkehr zu blockieren. Ihre Aktualisierungen sollten dennoch ausgeführt werden, obwohl der Benutzeroberflächen-Thread gesperrt ist. Ich würde kein ManualResetEventSlim verwenden, sondern nur eine einfache wait() und eine einzelne Aufgabe ohne eine Fortsetzung. Der Grund hierfür ist, dass standardmäßig Task.Run verhindert, dass die untergeordnete Task (Ihre Fortsetzung) an das übergeordnete Element angefügt wird und Ihre Fortsetzung möglicherweise nicht abgeschlossen werden kann, bevor das Fenster geschlossen wird, siehe 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() => 
     { 
      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 { })); 
     } 
    } 
} 

Sie können auch TaskFactory.StartNew mit TaskCreationOptions.AttachedToParent verwenden, wenn Sie wirklich eine Fortsetzung verwenden müssen.

EDIT: Ich habe meine Antwort mit @ Saeb Amini kombiniert, es ist ein bisschen ein Hack insgesamt, aber Sie behalten einige UI-Reaktionsfähigkeit.

EDIT 2: Hier ist eine Probe Demonstration der Lösung (mit einem neuen WPF-Projekt getestet):

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

Ja, einfach und nett. Ich bin mir nicht sicher, warum ich das nicht sehen könnte ... Ich werde jetzt versuchen, dies umzusetzen und mit einem Akzeptieren zu füttern. Vielen Dank für Ihre Zeit. – MoonKnight

+0

Das einzige Problem, das ich dabei finde, ist, dass meine 'NotifyOnPropertyChanged'-Delegaten beim Abrufen des ausgewählten Tests verwendet werden. Das Aktualisieren des' StatusMessage'-Threads wird blockiert, da ich auf den UI-Thread warte. Ich muss das pulsieren, bis ich mit meiner Arbeit fertig bin, aber wie in dieser Situation? – MoonKnight

+0

Ah Ich nahm an, dass Sie keine UI-Updates in dieser Methode, ich aktualisieren die Antwort –

4

Sie Application.DoEvents etwas ähnliches wie WinForm die verwenden können, aber für WPF, geht es um eine Flag verwendet, Ihre Brennen task, nichtWait, sondern kontinuierlich die UI-Meldungen in einer Schleife verarbeitet, bis Ihre Aufgabe erledigt ist, und setzt das Flag. z.B.:

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

Es ist ein bisschen hacky, aber Ihre Situation und keine Kontrolle über den anrufenden Code gegeben, könnte es die einzige Möglichkeit sein, eine ansprechende Benutzeroberfläche zu erhalten, ohne sofort mit dem Anrufer zurück.

+0

Vielen Dank dafür, Ihre Zeit wird am meisten geschätzt ... – MoonKnight

+0

@Killercam yw :) –

+0

Die while-Klausel ist besonders interessant für diejenigen, die nicht vertraut sind, wie SynchronizationContext funktioniert ... – Fazi

0

Ich versuchte die async/warte Kombination, um diese Art von Problem zu lösen. Zuerst konvertieren wir die Sync void CanClose in async void. Dann ruft die async void-Methode die asynchrone Task-Methode auf, um die Arbeit zu erledigen. Wir müssen dies tun, weil die Gefahr von Async beim Abfangen von Ausnahmen ungültig wird.

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

Meiner Meinung nach gibt es Vorteile der Verwendung dieses Ansatzes:

  • leichter zu verfolgen und zu verstehen
  • leichter Ausnahmebehandlung

Hinweis:

  • Ich habe den Storno-Tok weggelassen de im Code-Snippet, das einfach hinzugefügt werden kann, wenn Sie möchten.
  • async/erwarten keywords existiert nach .net Framework 4.5 und C# 5.0