Cross-Threading invalido su Operazione Asincrona

giovedì 24 luglio 2008 - 17.54

netaddicted Profilo | Newbie

Ciao, rieccomi con un enorme punto di domanda sulla testa.

ho un problema sulla chiamata asincrona a un Web
Service.

Vado punto per punto:

nel codice ho questa delegate:
"Public Delegate Sub AsyncMethodCaller(ByVal a As Integer, ByVal b As Integer, _
ByRef q As Integer, ByRef r As Integer)"

questa delegate ha la firma di un web service che voglio chiamare.

Il web service è istanziato in una variabile globale del form che
si chiama "_Operazioni".

Una procedura chiamata 'btnAsyncDivInt_Click' gestisce il click di un pulsante
chiamato appunto btnAsyncDivInt', che effettua la chiamata asincrona
in questo modo:

1_ creo una istanza delle delega passando il metodo del web service che
si chiama 'DivisioneIntera', quindi:

"Dim caller As New AsyncMethodCaller(AddressOf _Operazioni.DivisioneIntera)"

2_ Ottengo i primi 2 parametri da passare alla delegate. Si tratta del contenuto di 2
textbox da convertire in intero:

Dim a, b As Integer
a = Convert.ToInt32(txtA.Text)
b = Convert.ToInt32(txtB.Text)

3_ Finalmente faccio la chiamata asincrona e la assegno a una variabile di tipo
"IAsyncResult":

Dim AsyRes As IAsyncResult = caller.BeginInvoke(a, b, qGLOB, rGLOB, _
AddressOf Callback, caller)

Vorrei fare notare che gli ultimi 2 parametri del metodo chiamato (e cioè le variabili passate
per riferimento: "qGLOB" e "rGLOB") sono globali.

La procedura "Callback" è quella classica usata per recuperare il risultato
di una chiamata asincrona, ecco il codice:

"Public Sub Callback(ByVal AsyRes As IAsyncResult)
' Ritrovo la delegate...
Dim caller As AsyncMethodCaller = CType(AsyRes.AsyncState, AsyncMethodCaller)

'Il risultato della chiamata consiste nel settaggio delle 2 variabili globali
'passate per riferimento:
caller.EndInvoke(qGLOB, rGLOB, AsyRes)

'QUI' ARRIVANO I GUAI!
Me.txtQ.Text = qGLOB.ToString
Me.txtR.Text = rGLOB.ToString

End Sub"

Quando viene eseguita la prima istruzione che segue il commento:
"QUI' ARRIVANO I GUAI", ricevo questo errore:

"Operazione cross-thread non valida: è stato eseguito l'accesso al controllo 'txtQ'
da un thread diverso da quello da cui è stata eseguita la creazione."

Quindi deduco che la procedura "Callback" viene processata su un thread separato
a cui non è consentito modificare controlli creati da un altro thread.

C'è una soluzione (altra che settare la proprietà "CheckForIllegalCrossThreadCalls"
su "false") per effettuare la chiamata in "thread-safe" mode?

Se qualcuno è ferrato, potrebbe spiegarmi il concetto che sta dietro alla soluzione?
Vorrei capire.

Forse bisogna creare un riferimento al form nella procedura "Callback"
in un modo complesso?

Grazie per l'aiuto,
Francesco Torre (MCP .Net Developer)

rossimarko Profilo | Guru

Ciao,

penso che questo articolo faccia al caso tuo: http://msdn.microsoft.com/en-us/library/ms171728.aspx

Devi verificare la proprietà invokeRequired e se è = true allora usa il metodo Invoke. Nel link che ti ho mandato trovi un esempio a riguardo.
-----------------------------------------
Rossi Marco
http://blogs.dotnethell.it/rossimarko

netaddicted Profilo | Newbie

Grazie mille !!! Ho risolto leggendo l'articolo. L'errore era nella callback quando tentavo di impostare direttamente i controlli:

Private Sub Callback(ByVal AsyRes As IAsyncResult)

Dim caller As AsyncMethodCaller
caller = CType(AsyRes.AsyncState, AsyncMethodCaller)

caller.EndInvoke(qGLOB, rGLOB, AsyRes)

txtQ.Text = qGLOB.ToString 'ERRORE!
txtR.Text = rGLOB.ToString 'ERRORE!

End Sub

-----------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------

SOLUZIONE:

------------------------------------------------------------------------------------------------------
1. Creare una procedura o funzione nella classe del form che setti il valore del
controllo utilizzando questa forma:

If Me.<CONTROL>.InvokeRequired Then
Dim <NOMEDELEGATE> As New <DELEGATE>(AddressOf <METHOD>)
Me.Invoke(<NOMEDELEGATE>, Nothing)
Else
Me.<CONTROLLO>.<PROPRIETA CONTROLLO> = <VALORE>
End If

per la delegate da utilizzare, si veda il punto 2

NOTA: il secondo parametro dell’invocazione dovrebbe essere impostato ad un array
di tipo “Object” e contenente i parametri del metodo chiamato.
Poiché “SetText” non ha argomenti, possiamo tranquillamente porlo a “Nothing”:

Ad esempio, per la textbox "txtQ" abbiamo:

If Me.txtQ.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)
Else
Me.txtQ.Text = qGLOB.ToString
End If

"SetText" è il nome (arbitrario) della procedura che include questo ciclo
"If" (che quindi chiama se stessa, vedremo in seguito perchè).

La procedura completa, in questo caso, è la seguente:

Private Sub SetText()

If Me.txtQ.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)
Else
Me.txtQ.Text = qGLOB.ToString
End If

If Me.txtR.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)
Else
Me.txtR.Text = rGLOB.ToString
End If

End Sub

------------------------------------------------------------------------------------------------------
2. Creare un delegato per la procedura creata sopra, in questo caso:
Private Delegate Sub SetTextCallback()

Si noti che, in tutto il codice, questa delegate viene utilizzata solo nel punto 1

------------------------------------------------------------------------------------------------------
3. Invocare nella callback della chiamata asincrona la procedura creata al punto 1
DIRETTAMENTE (cioè: senza l'utilizzo di delega):

Private Sub Callback(ByVal AsyRes As IAsyncResult)
Dim caller As AsyncMethodCaller
caller = CType(AsyRes.AsyncState, AsyncMethodCaller)

caller.EndInvoke(qGLOB, rGLOB, AsyRes)

Me.SetText()
End Sub

Spiegazione:
Quando chiamiamo “SetText” per la prima volta, dall'interno
della callback asincrona, il test seguente restituisce "True":

If Me.txtQ.InvokeRequired...

Questo in "dotnettese", significa che che l’ID del Thread che ha generato la chiamata (la Sub Callback)
non corrisponde al Thread corrente (il Form, in parole semplici).

Allora facciamo in modo che sia proprio il Thread che ha generato i controlli a fare la chiamata
digitando:

Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)

Adesso è il form ad effettuare la chiamata (infatti è: “Me.Invoke...”).

La seconda 'volta che “SetText” viene invocata (dal Form, come abbiamo visto)
la proprietà “InvokeRequired” del controllo restituisce “False”.

Questo in "dotnettese", significa che che l’ID del Thread che ha generato la chiamata (il Form) corrisponde al Thread corrente
(siamo ancora sul Form); quindi posso impostare il controllo direttamente:

Else
Me.txtQ.Text = qGLOB.ToString
End If

-----------------------------------------------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------------------------------------------
POSTO IL CODICE COMPLETO DI COMMENTI:

Public Class Form1

'Nell’evento “Load” istanzio il web service:
Private Sub Form1_Load(ByVal sender As Object, ByVal e _
As System.EventArgs)Handles Me.Load
_Operazioni = New MathWS.Operazioni
End Sub

'Creiamo 2 variabili globali i cui valori saranno assegnati dal web service 'mediante passaggio per riferimento:
Private qGLOB As Integer = 0
Private rGLOB As Integer = 0

'La procedura seguente assegna le 'textbox con le variabili globali:
Private Sub SetText()

If Me.txtQ.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)
Else
Me.txtQ.Text = qGLOB.ToString
End If

If Me.txtR.InvokeRequired Then
Dim d As New SetTextCallback(AddressOf SetText)
Me.Invoke(d, Nothing)
Else
Me.txtR.Text = rGLOB.ToString
End If
End Sub

'Per la procedura creata appena sopra, bisogna definire una delega:
Private Delegate Sub SetTextCallback()

'Creiamo la delegate per la chiamata asincrona che dovrà avere la stessa firma 'del metodo
'del web service (“_Operazioni.DivisioneIntera”):
Private Delegate Sub AsyncMethodCaller(ByVal a As Integer, _
ByVal b As Integer, ByRef q As Integer, ByRef r As Integer)

'Sul form il pulsante che effettuerà la chiamata asincrona, si chiama '”btnAsyncDivInt”;
'quindi definiamo l’impalcatura per gestirne l’evento “Click”:
Private Sub btnAsyncDivInt_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnAsyncDivInt.Click

'Istanziamo la delega “AsyncMethodCaller” creata precedentemente e referenziamo 'il metodo del web service:
Dim caller As New AsyncMethodCaller(AddressOf _
_Operazioni.DivisioneIntera)

'I primi 2 parametri richiesti dal metodo del web service sono di tipo intero e si ottengono
'convertendo le stringhe immesse dall’utente in due textbox: “txtA” e ' “txtB”:
Dim a, b As Integer
a = Convert.ToInt32(Double.Parse(txtA.Text))
b = Convert.ToInt32(Double.Parse(txtB.Text))

'Creiamo l’oggetto di tipo “IAsyncResult” che conterra il riferimento alla 'chiamata asincrona:
Dim AsyRes As IAsyncResult

'Effettuiamo la chiamata asincrona passando come argomenti: 1)le variabili 'richieste da
'“_Operazioni.DivisioneIntera” (due variabili passate per valore: “a” 'e “b” più due variabili
'passate per riferimento - le globali “qGLOB” e “rGLOB” -),
2) 'il riferimento alla seconda callback (quella classica che riceve come argomento
'l’oggetto IAsyncResult e contiene la fine chiamata che restituisce il 'risultato)
'e infine 3) l’istanza della delegate stessa (caller):
AsyRes = caller.BeginInvoke(a, b, qGLOB, rGLOB, AddressOf Callback, _
caller)
End Sub

'Definiamo l’impalcatura della callback che termina la chiamata e restituisce il 'risultato:
Private Sub Callback(ByVal AsyRes As IAsyncResult)

'Recuperiamo il riferimento alla delegate che ha effettuato la chiamata
'asincrona:
Dim caller As AsyncMethodCaller
caller = CType(AsyRes.AsyncState, AsyncMethodCaller)

'Infine chiamiamo “EndInvoke”.
'In questo caso bisogna specificare tra parentesi:
'1) I valori che saranno 'restituiti(in questo caso le variabili globali “qGLOB” e “rGLOB)
'e 2) L’oggetto 'IAsyncResult 'stesso:
caller.EndInvoke(qGLOB, rGLOB, AsyRes)

'NO -> txtQ.Text = qGLOB.ToString
'NO -> txtR.Text = rGLOB.ToString


'Impostiamo indirettamente le textbox chiamando “SetText”:
Me.SetText()
End Sub
End Class

CIAO

Francesco Torre (MCP .Net Developer)


Partecipa anche tu! Registrati!
Hai bisogno di aiuto ?
Perchè non ti registri subito?

Dopo esserti registrato potrai chiedere
aiuto sul nostro Forum oppure aiutare gli altri

Consulta le Stanze disponibili.

Registrati ora !
Copyright © dotNetHell.it 2002-2024
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5