Ασύγχρονο fetch δεδομένων για control population


Έχουμε μια φόρμα και θέλουμε να κάνουμε 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.



Σχολιάστε

Εισάγετε τα παρακάτω στοιχεία ή επιλέξτε ένα εικονίδιο για να συνδεθείτε:

Λογότυπο WordPress.com

Σχολιάζετε χρησιμοποιώντας τον λογαριασμό WordPress.com. Αποσύνδεση / Αλλαγή )

Φωτογραφία Twitter

Σχολιάζετε χρησιμοποιώντας τον λογαριασμό Twitter. Αποσύνδεση / Αλλαγή )

Φωτογραφία Facebook

Σχολιάζετε χρησιμοποιώντας τον λογαριασμό Facebook. Αποσύνδεση / Αλλαγή )

Φωτογραφία Google+

Σχολιάζετε χρησιμοποιώντας τον λογαριασμό Google+. Αποσύνδεση / Αλλαγή )

Σύνδεση με %s