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

Έχουν δημοσιευτεί 26 Φεβρουαρίου 06 11:28 πμ | KelMan 

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

Σχόλια:

Χωρίς Σχόλια
Έχει απενεργοποιηθεί η προσθήκη σχολίων από ανώνυμα μέλη

Search

Go

Συνδρομές