Domanda

Ho un semplice UserControl per il paging del database, che utilizza un controller per eseguire le chiamate DAL effettive. Uso un BackgroundWorker per eseguire il sollevamento di carichi pesanti, e sull'evento OnWorkCompleted riattivo alcuni pulsanti, cambio una proprietà TextBox.Text e genera un evento per il modulo genitore.

Il modulo A contiene il mio UserControl. Quando faccio clic su un pulsante che si apre dal modulo B, anche se non faccio nulla " c'" e basta chiuderlo e provare a richiamare la pagina successiva dal mio database, OnWorkCompleted viene chiamato sul thread di lavoro (e non sul mio thread principale) e genera un'eccezione cross-thread.

Al momento ho aggiunto un segno di spunta per InvokeRequired nel gestore lì, ma non è necessario chiamare l'intero thread OnWorkCompleted sul thread principale? Perché non dovrebbe funzionare come previsto?

EDIT:

Sono riuscito a restringere il problema ad arcgis e BackgroundWorker . Ho la seguente soluzione che aggiunge un comando ad arcmap, che apre un semplice Form1 con due pulsanti.

Il primo pulsante esegue un BackgroundWorker che dorme per 500 ms e aggiorna un contatore. Nel metodo RunWorkerCompleted controlla InvokeRequired e aggiorna il titolo per mostrare a prescindere dal fatto che il metodo fosse originariamente in esecuzione all'interno del thread principale o del thread di lavoro. Il secondo pulsante apre appena Form2 , che non contiene nulla.

Inizialmente, tutte le chiamate a RunWorkerCompletedare sono fatte all'interno del thread principale (Come previsto - questo è il punto di svolta del metodo RunWorkerComplete, almeno da quello che ho capito da MSDN su BackgroundWorker )

Dopo aver aperto e chiuso Form2 , il RunWorkerCompleted viene sempre chiamato sul thread di lavoro. Voglio aggiungere che posso semplicemente lasciare questa soluzione al problema così com'è (controllare InvokeRequired nel metodo RunWorkerCompleted ), ma voglio capire perché sta accadendo contro le mie aspettative. Nel mio "reale" codice Mi piacerebbe sempre sapere che il metodo RunWorkerCompleted viene chiamato sul thread principale.

Sono riuscito a individuare il problema con il comando form.Show (); nel mio BackgroundTesterBtn - se utilizzo ShowDialog () invece, non ottengo alcun problema ( RunWorkerCompleted viene sempre eseguito sul thread principale). Devo usare Show () nel mio progetto ArcMap, in modo che l'utente non sia vincolato al modulo.

Ho anche provato a riprodurre il bug su un normale progetto WinForms. Ho aggiunto un semplice progetto che apre il primo modulo senza ArcMap, ma in quel caso non sono riuscito a riprodurre il bug: RunWorkerCompleted è stato eseguito sul thread principale, sia che avessi usato Show () o ShowDialog () , prima e dopo l'apertura di Form2 . Ho provato ad aggiungere un terzo modulo per fungere da modulo principale prima del mio Form1 , ma non ha modificato il risultato.

Qui è il mio semplice sln (VS2005sp1) - richiede

ESRI.ArcGIS.ADF (9.2.4.1420)

ESRI.ArcGIS.ArcMapUI (9.2.3.1380)

ESRI.ArcGIS.SystemUI (9.2.3.1380)

È stato utile?

Soluzione

Sembra un bug:

http://connect.microsoft.com/VisualStudio/feedback /ViewFeedback.aspx?FeedbackID=116930

http://thedatafarm.com/devlifeblog/archive/2005 /12/21/39532.aspx

Quindi suggerisco di usare il bullet-proof (pseudocodice):

if(control.InvokeRequired)
  control.Invoke(Action);
else
  Action()

Altri suggerimenti

  

L'intero punto di OnWorkCompleted non deve essere chiamato sul thread principale? Perché non dovrebbe funzionare come previsto?

No, non lo è.
Non puoi semplicemente eseguire qualsiasi cosa vecchia su qualsiasi vecchio thread. I thread non sono oggetti educati che puoi semplicemente dire " esegui questo, per favore " ;.

Un modello mentale migliore di un thread è un treno merci. Una volta che sta andando, è sulla sua propria traccia. Non puoi cambiare rotta o fermarla. Se vuoi influenzarlo, devi aspettare fino a quando non arriva alla prossima stazione ferroviaria (es: fai controllare manualmente alcuni eventi), o deraglialo ( Thread.Abort e le eccezioni di CrossThread hanno le stesse conseguenze del deragliare un treno ... attenzione!).

I controlli Winforms sorta di supportano questo comportamento (hanno Control.BeginInvoke che ti consente di eseguire qualsiasi funzione sul thread dell'interfaccia utente), ma funziona solo perché hanno un gancio speciale nella pompa messaggi dell'interfaccia utente di Windows e scrivere alcuni gestori speciali. Per andare con l'analogia di cui sopra, il loro treno effettua il check-in alla stazione e cerca periodicamente nuove direzioni, e puoi usare quella struttura per pubblicare le tue indicazioni.

BackgroundWorker è progettato per scopi generici (non può essere collegato alla GUI di Windows), quindi non può utilizzare le funzionalità di Windows Control.BeginInvoke . Deve presumere che il thread principale sia un "treno" inarrestabile che fa le sue cose, quindi l'evento completato deve essere eseguito nel thread di lavoro o per niente.

Tuttavia, quando si utilizzano winforms, nel gestore OnWorkCompleted , è possibile ottenere la finestra per eseguire un altro callback utilizzando il BeginInvoke funzionalità che ho menzionato sopra. In questo modo:

// Assume we're running in a windows forms button click so we have access to the 
// form object in the "this" variable.
void OnButton_Click(object sender, EventArgs e )
    var b = new BackgroundWorker();
    b.DoWork += ... blah blah

    // attach an anonymous function to the completed event.
    // when this function fires in the worker thread, it will ask the form (this)
    // to execute the WorkCompleteCallback on the UI thread.
    // when the form has some spare time, it will run your function, and 
    // you can do all the stuff that you want
    b.RunWorkerCompleted += (s, e) { this.BeginInvoke(WorkCompleteCallback); }
    b.RunWorkerAsync(); // GO!
}

void WorkCompleteCallback()
{
    Button.Enabled = false;
    //other stuff that only works in the UI thread
}

Inoltre, non dimenticare questo:

  

Il gestore eventi RunWorkerCompleted deve sempre controllare le proprietà Error e Canceled prima di accedere alla proprietà Result. Se è stata sollevata un'eccezione o l'operazione è stata annullata, l'accesso alla proprietà Result genera un'eccezione.

Il BackgroundWorker verifica se l'istanza delegata punta a una classe che supporta l'interfaccia ISynchronizeInvoke . Il tuo livello DAL probabilmente non implementa tale interfaccia. Normalmente, useresti BackgroundWorker su un Form , che supporta quell'interfaccia.

Nel caso in cui si desideri utilizzare BackgroundWorker dal livello DAL e si desidera aggiornare l'interfaccia utente da lì, sono disponibili tre opzioni:

  • continueresti a chiamare il metodo Invoke
  • implementa l'interfaccia ISynchronizeInvoke sulla classe DAL e reindirizza le chiamate manualmente (sono solo tre metodi e una proprietà)
  • prima di invocare BackgroundWorker (quindi, nel thread dell'interfaccia utente), per chiamare SynchronizationContext.Current e per salvare l'istanza del contenuto in una variabile di istanza. SynchronizationContext ti darà quindi il metodo Send , che farà esattamente ciò che Invoke .

L'approccio migliore per evitare problemi con il cross-threading nella GUI è usa SynchronizationContext .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top