Αν εκεί που δουλεύετε πάρει φωτιά το PC, θα είναι τιμωρία από τον θεό της πληροφορικής βρε αθεόφοβοι
(cap και kkara)!
Λοιπόν, για να σοβαρευτούμε λίγο… Το να βάλεις τόσα Application.DoEvents, όσα απαιτούνται για να προλάβει να σχεδιαστεί η φόρμα είναι τουλάχιστον μπακαλίστικο! Υπάρχουν άλλοι κομψότεροι τρόποι, όπως τα threads αλλά και άλλοι κομψότατοι τρόποι όπως τα delegates. Τα τελευταία μάλιστα στο VS2005 μας παρέχονται και σε μορφή component για όσους τους αρέσει το κλίκι-κλίκι…
Όπως και να έχει, το ερώτημα είναι (όπως πάντοτε άλλωστε) «τι θέλω να κάνω;». Δηλαδή, ας υποθέσουμε ότι έχουμε μια φόρμα που απαιτεί κάποια ώρα για να σηκωθεί. Βεβαίως, βεβαίως μπορούμε να πάρουμε το κομμάτι του κώδικα που ευθύνεται και να το βάλουμε να τρέξει, με κάποιο τρόπο, παράλληλα ώστε να ελαφρύνει η φόρμα. Το θέμα είναι, μέχρι να ολοκληρωθεί αυτό το lro (long-running-operation), ο χρήστης τι κάνει; Μπορεί να κάνει κάτι άλλο; Ή μήπως είναι καταδικασμένος, προκειμένου να μπορέσει να δουλέψει, να περιμένει να τελειώσει το lro? Zaxos, υποθέτω ότι αυτό είναι το σενάριο σου… Σε αυτήν τη δεύτερη περίπτωση, αυτό που θα πρέπει υποχρεωτικά να κάνουμε είναι να εμφανίσουμε την please-wait φόρμα.
Μιας και όταν τα λέγαμε αυτά στο Φορμα "Please wait" κατά την εκτέλεση απαιτητικής σε χρόνο διαδικασίας, ο cap τα ξέχασε
, ας δούμε λοιπόν, τι επιλογές έχουμε για να υλοποιήσουμε ένα fetch-data-and-populate-control σενάριο…
Έχουμε μια φόρμα και θέλουμε να κάνουμε populate ένα listbox με data. O κώδικας που μας επιστρέφει τα data είναι ο παρακάτω:
Function GetCustomerList(ByVal State As String) As String()
'*** call across network to DBMS or Web Services to retrieve data
'*** pass data back to caller using string array return value
Threading.Thread.CurrentThread.Sleep(3000)
Return "1000,1001,1002,1003".Split(",")
End Function
Επίσης, θα χρειαστούμε ένα delegate object δηλωμένο σε επίπεδο κλάσης (φόρμας):
Delegate Function GetCustomerListHandler(ByVal State As String) As String()
Η παράμετρος state στο function δεν χρησιμοποιείται αλλά την έχω βάλει μόνο για να δείτε πως μπορούμε να περνάμε και παραμέτρους κατά τις ασύγχρονες κλήσεις.
1η επιλογή: Σύγχρονο fetch
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
'*** create delegate object and bind to target method
Dim handler1 As GetCustomerListHandler
handler1 = AddressOf GetCustomerList
'*** execute method synchronously
Dim retval As String()
retval = handler1.Invoke("dummy")
ListBox1.Items.AddRange(retval)
End Sub
Θα μου πείτε, γιατί να το κάνουμε έτσι, αφού θα μπορούσαμε να πούμε
ListBox1.Items.AddRange(GetCustomerList)
Πράγματι, ο κώδικας είναι ισοδύναμος ως προς το αποτέλεσμα, αλλά ο λόγος είναι απλά για να εξοικιωθούμε με τα delegates και επίσης να δούμε πως το ίδιο delagate object εξυπηρετεί πολλαπλούς τρόπους κλήσης (sync/async). Αν δεν έχετε ξαναδεί delegate θα σας φανεί λίγο περίεργο, δηλαδή να δηλώνουμε Delegate Function GetCustomerListHandler {…} και κατόπιν Dim handler1 As GetCustomerListHandler. Απλά φαναστείτε ότι με το πρώτο, ορίζουμε έναν τύπο από delegate. Είναι σαν να λέμε:
Private Enum WeatherEnum
Sunny
Cloudy
Rainy
End Enum
Private myWeather As WeatherEnum
με τη διαφορά ότι στα delegates ορίζουμε και instance ταυτόχρονα!
2η επιλογή: Ασύγχρονα delegates με polling
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
'*** create delegate object and bind to target method
Dim handler1 As GetCustomerListHandler = AddressOf GetCustomerList
'*** execute method asynchronously
Dim ar As System.IAsyncResult
ar = handler1.BeginInvoke("dummy", Nothing, Nothing)
Timer1.Enabled = True
' Do whatever you have to do
'check to see if the async call has completed before continue
Do
Application.DoEvents() '
Loop Until ar.IsCompleted
Timer1.Enabled = False
Dim retval As String()
Try
'if you ommit the loop above, the EndInvoke looks like a blocking call
retval = handler1.EndInvoke(ar)
ListBox1.Items.AddRange(retval)
Catch ex As Exception
Debug.WriteLine(ex.Message)
End Try
End Sub
Αυτήν τη φορά, αντί για Invoke, καλούμε την BeginInvoke του delegate. Παρατηρήστε ότι περνάμε στην παράμετρο state την τιμή “dummy” , ενώ η χρησιμότητα των δύο nothing παραμέτρων θα φανεί παρακάτω. Το σενάριο εδώ λέει, «ξεκίνα να τρέχεις παράλληλα το function, εγώ έχω λίγη δουλειά να κάνω, αν τελειώσω, πέφτω σε loop μέχρει να τελειώσεις κι εσύ». Οι πληροφορίες για το σε τι κατάσταση βρίσκεται η εκτέλεση του delegate βγαίνουν μέσα από το IAsyncResult το οποίο μας δίνει πρόσβαση στο IsCompleted property και την μέθοδο EndInvoke. Όλο το ζουμί είναι εκεί. Μέχρι να την καλέσουμε, δεν παίρνουμε πίσω αποτελέσματα, ενώ αν την καλέσουμε νωρίτερα (πριν να ολοκληρωθεί), έχουμε ένα blocking call (πέφτουμε στην 1η περίπτωση). Αυτό το σενάριο είναι κατάλληλο για υλοποίηση wait-form.
3η επιλογή: Callback delegates
Εδώ αρχίζουν τα ενδιαφέροντα…
'*** create delegate object to execute method asynchronously
Private TargetHandler1 As GetCustomerListHandler = AddressOf GetCustomerList
'*** create delegate object to service callback from CLR
Private CallbackHandler1 As AsyncCallback = AddressOf MyCallbackMethod1
Ορίζουμε ένα νέο delegate object, καθως επίσης κι ένα AsyncCallback delegate. Αυτό το δεύτερο είναι ένα ειδικό delegate το οποίο σχετίζεται με ένα function (MyCallbackMethod1) το οποίο θα τρέξει, όταν ολοκληρωθεί το invocation.
Sub cmdExecuteTask1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button3.Click
'*** execute method asynchronously with callback
TargetHandler1.BeginInvoke("dummy", CallbackHandler1, Nothing)
Timer1.Enabled = True
End Sub
Παρατηρείστε ότι αυτήν τη φορά με το BeginInvoke περνάμε και ως παράμετρο το AsyncCallback delegate.
'*** callback method runs on worker thread and not the UI thread
Sub MyCallbackMethod1(ByVal ar As IAsyncResult)
'*** this code fires at completion of each asynchronous method call
Dim retval As String()
retval = TargetHandler1.EndInvoke(ar)
' **********************************************************************************************
ListBox1.Items.AddRange(retval) 'This is illegal... UI should not be updated from async threads!
' **********************************************************************************************
Timer1.Enabled = False
End Sub
Και εδώ πλέον κάνουμε handle τα αποτελέσματα από την ασύγχρονη εκτέλεση. Εδώ είναι ένα σημείο που χρειάζεται προσοχή! Αν αυτό που γίνεται είναι κάτι που γίνεται στο background, όσο συνεχίζει να δουλεύει ο χρήστης, και μετά απλώς τελειώνει τότε έχει καλώς. Αν όμως, ανάλογα των αποτελεσμάτων, αποφασίσουμε να κάνουμε δίαφορες ενέργειες στο UI, τότε ο κώδικας (όπως παραπάνω, που καλούμε την AddRange(retval)) είναι ακατάλληλος. Γιατί, αυτό το function (MyCallbackMethod1) μπορεί να συνυπάρχει οπτικά μαζί με τον υπόλοιπο κώδικα της φόρμας, εντούτoις εκτελείται, όταν έρθει η ώρα, σε διαφορετικό thread και όπως έχουμε πει ΤΑ CONTROLS TA ΠΕΙΡΑΖΟΥΜΕ ΜΟΝΟ ΜΕΣΑ ΑΠΟ ΤΟ THREAD ΣΤΟ ΟΠΟΙΟ ΑΝΗΚΟΥΝ. Γι αυτόν το λόγο, έχουμε την τέταρτη και τελευταία υλοποίση του σεναρίου.
4η επιλογή: Callback delegates με UI update
Εδώ θα αλλάξουμε λίγο το παράδειγμά μας και θα υποθέσουμε ότι τα data μας τα επιστρέφει ένα object που έχει αναλάβει το data access και η μέθοδος που καλούμε είναι η εξής:
Public Function GetAllCustomers() As DataTable
Άρα χρειαζόμαστε
'*** a delegate for executing handler methods
Delegate Function GetAllCustomersHandler() As DataTable
Να δηλώσουμε ένα νέο delegate object, κατάλληλο για να διαχειριστεί το signature του GetAllCustomers
'*** create delegate object to execute method asynchronously
Private TargetHandler2 As GetAllCustomersHandler = AddressOf oCustomers.GetAllCustomers
Να ορίσουμε ένα delegate, τύπου GetAllCustomersHandler, το οποίο θα ξεκινήσει τη διαδικασία
'*** create delegate object to service callback from CLR
Private CallbackHandler2 As AsyncCallback = AddressOf MyCallbackMethod2
Να ορίσουμε ένα δεύτερο delegate object το οποίο θα τρέξει τον κώδικα που θα εκτελεστεί όταν τελειώση η διαδικασία.
'*** delegate used to switch control over to primary UI thread
Delegate Sub UpdateUIHandler(ByVal StatusMessage As String, ByVal dtCustomers As DataTable)
Και τέλος, ένα τρίτο delegate το οποίο θα μας επιτρέψει να χειριστούμε τα data στο UI από όπου όλα ξεκίνησαν.
Ο αντίστοιχος κώδικας έχει ως εξής:
Sub cmdExecuteTask_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button4.Click
'*** execute method asynchronously with callback
UpdateUI("Starting task...", Nothing)
TargetHandler2.BeginInvoke(CallbackHandler2, Nothing)
End Sub
Όπως προηγουμένως. Απλά πλέον χρησιμοποιούμε μια ρουτίνα UpdateUI για να ενημερώσουμε το UI.
'*** callback method runs on worker thread and not the UI thread
Sub MyCallbackMethod2(ByVal ar As IAsyncResult)
Try
Dim retval As DataTable
retval = TargetHandler2.EndInvoke(ar)
UpdateUI("Task complete", retval)
Catch ex As Exception
Dim msg As String
msg = "Error: " & ex.Message
UpdateUI(msg, Nothing)
End Try
End Sub
Κι εδώ όπως προηγουμένως, μόνο που περνάμε τα αποτελέσματα στην UpdateUI.
Το μεγάλο ερώτημα είναι τι γίνεται στην UpdateUI…
'*** can be called from any method on form to update UI
Sub UpdateUI(ByVal StatusMessage As String, ByVal dtCustomers As DataTable)
'*** check to see if thread switch is required
If Me.InvokeRequired Then
Dim handler As New UpdateUIHandler(AddressOf UpdateUI_Impl)
Dim args() As Object = {StatusMessage, dtCustomers}
Me.BeginInvoke(handler, args)
Else
UpdateUI_Impl(StatusMessage, dtCustomers)
End If
End Sub
'*** this method always runs on primary UI thread
Sub UpdateUI_Impl(ByVal StatusMessage As String, ByVal Customers As DataTable)
Label1.Text = StatusMessage
ListBox1.DataSource = Customers
End Sub
Τσα! Ουσιαστικά, η δουλειά δεν γίνεται ακριβώς στην UpdateUI. Απλώς η UpdateUI ελέγχει από πού έρχεται αυτός που την καλεί. Αν έρχεται από διαφορετικό thread (InvokeRequired=true), τότε μέσω του UpdateUIHandler, επιστρέφουμε στο thread του UI και καλούμε την UpdateUI_Impl.
Με αυτά τα ολίγα έχουν καλυφθεί τέσσερα σενάρια περί fetch-data-and-populate-control. Aν υπάρχει κανένα πρόβλημα ή ερώτημα μπορούμε να συνεχίσουμε την κουβέντα μας…
Vir prudens non contra ventum mingit