2011-01-19 4 views
51


Ho un modulo con 2 caselle su di esso. E voglio riempire combobox2.DataSource sulla base di combobox1.Text e combobox2.Text (presumo che l'utente abbia completato l'immissione in combobox1 e si trovi nel mezzo dell'input in combobox2). Così ho un gestore eventi per combobox2 come questo:Come arrestare correttamente BackgroundWorker

private void combobox2_TextChanged(object sender, EventArgs e) 
{ 
    if (cmbDataSourceExtractor.IsBusy) 
     cmbDataSourceExtractor.CancelAsync(); 

    var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text, 
     V2 = combobox2.Text}; 
    cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues); 
} 

Per quanto riguarda la costruzione DataSource è un processo che richiede tempo (si crea una richiesta al database e lo esegue) ho deciso che è meglio eseguirlo in un altro processo usando BackgroundWorker. Quindi c'è uno scenario in cui cmbDataSourceExtractor non ha completato il suo lavoro e l'utente digita un altro simbolo. In questo caso ricevo un'eccezione su questa riga
cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues); a proposito del fatto che BackgroundWorker è occupato e non può eseguire più azioni nello stesso momento.
Come sbarazzarsi di questa eccezione?
Grazie in anticipo!

risposta

81

CancelAsync in realtà non interrompe il thread o qualcosa del genere. Invia un messaggio al thread di lavoro che dovrebbe essere annullato tramite BackgroundWorker.CancellationPending. Il delegato DoWork che viene eseguito in background deve controllare periodicamente questa proprietà e gestire la cancellazione stessa.

La parte difficile è che probabilmente il delegato DoWork sta bloccando, il che significa che il lavoro svolto su DataSource deve essere completato prima di poter eseguire qualsiasi altra operazione (ad esempio, selezionare CancellationPending). Potrebbe essere necessario spostare il lavoro effettivo in un altro delegato asincrono (o forse ancora meglio, inviare il lavoro allo ThreadPool) e avere il polling del thread principale del lavoro finché questo thread del lavoratore interno attiva uno stato di attesa, OPPURE rileva CancellationPending.

http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.cancelasync.aspx

http://www.codeproject.com/KB/cpp/BackgroundWorker_Threads.aspx

+1

Vorrei aggiungere che CancelAsync è un'operazione non bloccante quindi, anche se gestisci CancellationPendning correttamente in DoWork(), non puoi eseguire CancelAsync per passare alla riga successiva poiché DoWork() potrebbe terminare in pochi secondi . – dzendras

1

Il problema è causato dal fatto che cmbDataSourceExtractor.CancelAsync() è un metodo asincrono, l'operazione Cancel non ha ancora completato quando cmdDataSourceExtractor.RunWorkerAsync(...) exitst. È necessario attendere il completamento di cmdDataSourceExtractor prima di chiamare di nuovo RunWorkerAsync. Come fare questo è spiegato in this SO question.

24

Se si aggiunge un ciclo tra il CancelAsync() e il RunWorkerAsync() in questo modo si risolverà il vostro problema

private void combobox2_TextChanged(object sender, EventArgs e) 
{ 
    if (cmbDataSourceExtractor.IsBusy) 
     cmbDataSourceExtractor.CancelAsync(); 

    while(cmbDataSourceExtractor.IsBusy) 
     Application.DoEvents(); 

    var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text, 
     V2 = combobox2.Text}; 
    cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues); 
    } 

Il ciclo while con la chiamata a Application.DoEvents() sarà hault l'esecuzione di il tuo nuovo thread di lavoro fino a quando quello corrente non è stato cancellato correttamente, tieni presente che devi ancora gestire l'annullamento del thread di lavoro. Con qualcosa come:

private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e) 
{ 
     if (this.cmbDataSourceExtractor.CancellationPending) 
     { 
      e.Cancel = true; 
      return; 
     } 
     // do stuff... 
} 

I Application.DoEvents() nel primo frammento di codice continuerà a elaborare la vostra interfaccia grafica threads coda dei messaggi in modo che il persino di cancellare e aggiornare la proprietà cmbDataSourceExtractor.IsBusy sarà ancora da lavorare (se semplicemente aggiunto un continuo anziché Applicazione.DoEvents() il ciclo si chiudeva il filo GUI in uno stato occupato e non avrebbe elaborare l'evento per aggiornare il cmbDataSourceExtractor.IsBusy)

+0

Non ho potuto utilizzare questa soluzione perché non c'è Application.DoEvents() in WPF. Ho impostato un flag aggiuntivo quando il lavoratore è ancora occupato quando arriva una nuova richiesta. Quindi, quando viene impostato il flag, invoco RunWorkerAsync sul thread principale dal callback RunWorkerCompleted. – bor

0

Sono d'accordo con i ragazzi. Ma a volte devi aggiungere più cose.

IE

1) Aggiungere questo worker.WorkerSupportsCancellation = true;

2) Aggiungere a voi classe un metodo per fare le seguenti cose

public void KillMe() 
{ 
    worker.CancelAsync(); 
    worker.Dispose(); 
    worker = null; 
    GC.Collect(); 
} 

Quindi, prima di chiudere l'applicazione tuo devono chiamare questo metodo.

3) Probabilmente è possibile Dispose, null tutte le variabili e i timer che si trovano all'interno dello BackgroundWorker.

+2

Vedi qui perché non è necessario chiamare Dispose manualmente per un BackgroundWorker http://stackoverflow.com/questions/2542326/proper-way-to-dispose-of-a-backgroundworker –

+2

Solo per salvare i googler un paio di sessioni di debug, se il lavoratore ha un ciclo/o utilizza RunWorkerCompletedEventHandler e sta verificando la cancellazione, è necessario spostare i metodi Dispose, Null e Collect all'esterno in RunWorkerCompletedEventHandler. – HockeyJ

+1

Non dovresti mai chiamare GC.Collect(), dato che passerà in modo aggressivo attraverso l'intero grafo di oggetti dell'applicazione, che ucciderà le prestazioni delle app con un numero elevato di oggetti. – Vedran

4

IL MIO esempio. DoWork è qui sotto:

DoLengthyWork(); 

    //this is never executed 
    if(bgWorker.CancellationPending) 
    { 
     MessageBox.Show("Up to here? ..."); 
     e.Cancel = true; 
    } 

all'interno DoLenghtyWork:

public void DoLenghtyWork() 
{ 
    OtherStuff(); 
    for(int i=0 ; i<10000000; i++) 
    { int j = i/3; } 
} 

all'interno altraroba():

public void OtherStuff() 
{ 
    for(int i=0 ; i<10000000; i++) 
    { int j = i/3; } 
} 

Che cosa si vuole fare è modificare sia DoLenghtyWork e altraroba() in modo che diventino :

public void DoLenghtyWork() 
{ 
    if(!bgWorker.CancellationPending) 
    {    
     OtherStuff(); 
     for(int i=0 ; i<10000000; i++) 
     { 
      int j = i/3; 
     } 
    } 
} 

public void OtherStuff() 
{ 
    if(!bgWorker.CancellationPending) 
    { 
     for(int i=0 ; i<10000000; i++) 
     { 
      int j = i/3; 
     } 
    } 
} 
+4

So che è vecchio, ma è sbagliato ... Stai controllando se la cancellazione è in sospeso prima di fare QUALUNQUE del lavoro .....? –

0

Nel mio caso ho dovuto fare il pool di database per la conferma di pagamento per entrare e quindi aggiornare l'interfaccia utente WPF.

meccanismo che gira su tutti i processi:

public void Execute(object parameter) 
     { 
      try 
      { 
       var amount = ViewModel.Amount; 
       var transactionId = ViewModel.TransactionMain.TransactionId.ToString(); 
       var productCode = ViewModel.TransactionMain.TransactionDetailList.First().Product.ProductCode; 
       var transactionReference = GetToken(amount, transactionId, productCode); 
       var url = string.Format("{0}New?transactionReference={1}", Settings.Default.PaymentUrlWebsite, transactionReference); 
       Process.Start(new ProcessStartInfo(url)); 
       ViewModel.UpdateUiWhenDoneWithPayment = new BackgroundWorker {WorkerSupportsCancellation = true}; 
       ViewModel.UpdateUiWhenDoneWithPayment.DoWork += ViewModel.updateUiWhenDoneWithPayment_DoWork; 
       ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerCompleted += ViewModel.updateUiWhenDoneWithPayment_RunWorkerCompleted; 
       ViewModel.UpdateUiWhenDoneWithPayment.RunWorkerAsync(); 
      } 
      catch (Exception e) 
      { 
       ViewModel.Log.Error("Failed to navigate to payments", e); 
       MessageBox.Show("Failed to navigate to payments"); 
      } 
     } 

meccanismo che non il controllo di esecuzione:

private void updateUiWhenDoneWithPayment_DoWork(object sender, DoWorkEventArgs e) 
    { 
     Thread.Sleep(30000); 
     while (string.IsNullOrEmpty(GetAuthToken()) && !((BackgroundWorker)sender).CancellationPending) 
     { 
      Thread.Sleep(5000); 
     } 

     //Plug in pooling mechanism 
     this.AuthCode = GetAuthToken(); 
    } 

meccanismo che annulla se la finestra viene chiusa:

private void PaymentView_OnUnloaded(object sender, RoutedEventArgs e) 
    { 
     var context = DataContext as PaymentViewModel; 
     if (context.UpdateUiWhenDoneWithPayment != null && context.UpdateUiWhenDoneWithPayment.WorkerSupportsCancellation && context.UpdateUiWhenDoneWithPayment.IsBusy) 
      context.UpdateUiWhenDoneWithPayment.CancelAsync(); 
    } 
1

La mia risposta è un po 'diversa perché ho provato questi metodi ma non hanno funzionato. Il mio codice usa una classe extra che controlla un flag booleano in una classe statica pubblica mentre i valori del database vengono letti o dove preferisco poco prima che un oggetto venga aggiunto a un oggetto List o qualcosa del genere. Vedi la modifica nel codice qui sotto. Ho aggiunto la proprietà ThreadWatcher.StopThread. per questa esplosione non ho intenzione di ripristinare il thread corrente perché non è il tuo problema, ma è facile come impostare la proprietà su false prima di accedere al thread successivo ...

private void combobox2_TextChanged(object sender, EventArgs e) 
{ 
    //Stop the thread here with this 
    ThreadWatcher.StopThread = true;//the rest of this thread will run normally after the database function has stopped. 
    if (cmbDataSourceExtractor.IsBusy) 
     cmbDataSourceExtractor.CancelAsync(); 

    while(cmbDataSourceExtractor.IsBusy) 
     Application.DoEvents(); 

    var filledComboboxValues = new FilledComboboxValues{ V1 = combobox1.Text, 
     V2 = combobox2.Text}; 
    cmbDataSourceExtractor.RunWorkerAsync(filledComboboxValues); 
    } 

tutti bene

private void cmbDataSourceExtractor_DoWork(object sender, DoWorkEventArgs e) 
{ 
     if (this.cmbDataSourceExtractor.CancellationPending) 
     { 
      e.Cancel = true; 
      return; 
     } 
     // do stuff... 
} 

Ora aggiungere la seguente classe

public static class ThreadWatcher 
{ 
    public static bool StopThread { get; set; } 
} 

e nella classe in cui si legge il database

List<SomeObject>list = new List<SomeObject>(); 
... 
if (!reader.IsDbNull(0)) 
    something = reader.getString(0); 
someobject = new someobject(something); 
if (ThreadWatcher.StopThread == true) 
    break; 
list.Add(something); 
... 

non dimenticare di usare un blocco finalmente per chiudere correttamente i tuoi dati collegamento abase ecc. Spero che questo aiuti! Per favore segnalami se lo trovi utile.