2015-08-23 12 views
8

Ho un'applicazione WPF abbastanza complessa che (molto simile a VS2013) ha IDocuments e ITools ancorata nella shell principale dell'applicazione. Uno di questi Tools deve essere arrestato in modo sicuro quando la finestra principale viene chiusa per evitare di entrare in uno stato "cattivo". Quindi io uso il metodo public override void CanClose(Action<bool> callback) di Caliburn Micro per eseguire alcuni aggiornamenti del database, ecc. Il problema che ho è tutto il codice di aggiornamento in questo metodo utilizza MongoDB Driver 2.0 e questa roba è async. Qualche codice; Al momento sto cercando di eseguireAttendere il completamento dell'attività senza bloccare la filettatura dell'interfaccia utente

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

Ora, per cominciare non ho avuto il ManualResetEventSlim e questo sarebbe, ovviamente, tornare alla CanClose chiamante prima ho aggiornato il mio database sullo sfondo [filo-piscina] thread. Nel tentativo di impedire il ritorno fino a quando ho finito i miei aggiornamenti ho cercato di bloccare il ritorno, ma questo congela il thread dell'interfaccia utente e impedisce che qualcosa accada.

Come posso eseguire il mio codice di pulizia senza tornare al chiamante troppo presto?

Grazie per il vostro tempo.


Nota, non posso ignorare il OnClose metodo che utilizza la firma asincrono come il codice chiamante non attendere che (non ho alcun controllo su questo).

+0

Non dovrebbero chiamante prendere la decisione di chiudere la finestra in base alla richiamata e non se restituito dal metodo? Penserei che se si esegue il rollback del task e si ritorni immediatamente, la finestra non dovrebbe essere chiusa fino a quando non si effettua una callback (true/false). –

+0

Nah, sfortunatamente no. Se non si imposta la richiamata, Caliburn assume che sia stato 'callback (true)' che "dice" questo componente può essere chiuso. Se inserisco 'callback (false)' ofcourse, l'operazione close viene cancellata del tutto. – MoonKnight

+0

Potrei scrivere la mia classe di comportamento vicino ma penso che dovrei essere in grado di fare ciò che voglio qui senza andare in mare ... – MoonKnight

risposta

7

Non penso che tu abbia molta scelta che bloccare il ritorno. Tuttavia, gli aggiornamenti dovrebbero essere eseguiti nonostante il thread dell'interfaccia utente sia bloccato. Non userei un ManualResetEventSlim, ma solo una semplice wait() e una singola attività senza una continuazione. Il motivo è che per impostazione predefinita Task.Run impedisce all'attività figlia (la tua continuazione) di essere collegata al genitore e quindi la tua continuazione potrebbe non avere il tempo di completarsi prima che la finestra si chiuda, vedi 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 { })); 
     } 
    } 
} 

È inoltre possibile utilizzare TaskFactory.StartNew con TaskCreationOptions.AttachedToParent se si ha realmente bisogno di utilizzare una continuazione.

EDIT: Ho combinato la mia risposta con @Saeb Amini, è un po 'un trucco nel complesso ma si mantiene una certa reattività dell'interfaccia utente.

EDIT 2: Ecco una dimostrazione del campione della soluzione (testato con un nuovo progetto WPF):

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

Sì, semplice e bello. Non sono sicuro del motivo per cui non ho potuto vedere questo ... Avrò un tentativo di implementarlo ora e rispondere con un'accettazione. Grazie mille per il tuo tempo. – MoonKnight

+0

L'unico problema che sto riscontrando è che i miei delegati 'NotifyOnPropertyChanged' utilizzati durante il test Selected, l'aggiornamento del thread' StatusMessage' sono bloccati perché sto aspettando sul thread dell'interfaccia utente. Ho bisogno di pulsare questo fino a quando ho finito con il mio lavoro, ma come in questa situazione? – MoonKnight

+0

Ah, ho pensato che non stavi aggiornando l'interfaccia utente con questo metodo, sto aggiornando la risposta –

4

Si può usare qualcosa di simile a WinForm di Application.DoEvents ma per WPF, Si tratta di utilizzare una bandiera, sparando la tua task, nonWait per esso, ma processa continuamente i messaggi dell'interfaccia utente in un ciclo finché l'attività non viene completata e imposta il flag. ad es.:

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

E 'un po' hacky, ma data la vostra situazione e alcun controllo sul codice chiamante, potrebbe essere l'unica opzione per mantenere un reattivo interfaccia utente, senza immediatamente ritornare al chiamante.

+0

Grazie per questo, il tuo tempo è più apprezzato ... – MoonKnight

+0

@Killercam yw :) –

+0

La clausola while è particolarmente interessante per coloro che non hanno familiarità con il funzionamento di SynchronizationContext ... – Fazi

0

Ho provato la combinazione asincrona/attesa per risolvere questo tipo di problema. Innanzitutto convertiamo il vuoto di sincronizzazione CanClose in vuoto asincrono. Quindi il metodo del vuoto asincrono chiama il metodo Task asincrono per eseguire il lavoro. Dobbiamo farlo perché il pericolo di vuoto asincrono quando si osservano eccezioni.

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

A mio parere, ci sono vantaggi dell'utilizzo di questo approccio:

  • più facile da seguire e capire
  • più facile gestione delle eccezioni

Nota:

  • Ho omesso il tok di cancellazione nel frammento di codice, che può essere aggiunto facilmente se lo si desidera.
  • asincrone/attendono esistono parole chiave dopo .NET Framework 4.5 e C# 5.0
Problemi correlati