Πολλές φορές, όταν φτιάχνουμε μια Windows Forms εφαρμογή, χρειάζεται να εκτελέσουμε κάτι χρονοβόρο (π.χ. “τράβηγμα” πολλών στοιχείων από μια βάση δεδομένων). Σε αυτές τις περιπτώσεις, αν εκτελέσουμε τη χρονοβόρα διαδικασία σύγχρονα (δηλαδή από το ίδιο thread στο οποίο είναι η εφαρμογή μας) τότε παρατηρούμε ότι το παράθυρο “κολλάει” (Not Responding). Η λύση για αυτό το πρόβλημα είναι η εκτέλεση της χρονοβόρας διαδικασίας από διαφορετικό Thread.
Ενώ κάλλιστα μπορούμε να είτε να δημιουργήσουμε ένα καινούριο Thread είτε να χρησιμοποιήσουμε την ThreadPool (*), μια πολύ καλή μέθοδος για Windows Forms εφαρμογές είναι να χρησιμοποιήσουμε το BackgroundWorker component, το οποίο βρίσκεται στο namespace System.ComponentModel.
(*): Σε τέτοιες περιπτώσεις, καλό είναι να θυμόμαστε ότι τη φόρμα πρέπει να την πειράζουμε μόνο από το Thread στο οποίο εκτελείται. Περισσότερα για αυτό σε επόμενο blog post.
Αρχικά, κατασκευάζουμε μια Windows Forms εφαρμογή. Κατόπιν, κάνουμε drag drop έναν BackgroundWorker πάνω στη φόρμα
και βλέπουμε ότι δημιουργείται ένα instance του (με το default όνομα backgroundWorker1) στο κάτω μέρος του Windows Forms designer.
Στη συνέχεια, επειδή θέλουμε ο BackgroundWorker μας να υποστηρίζει και ακύρωση αλλά και να μας αναφέρει την πρόοδο της διαδικασίας που εκτελεί, πάμε στα properties του, και κάνουμε set σε true τα properties WorkerReportsProgress και WorkerReportsCancellation
Στη συνέχεια, ας δημιουργήσουμε event handlers για τα παρακάτω events
Για να δείξουμε τη λειτουργία του BackgroundWorker, θα κάνουμε drag drop στη φόρμα δύο buttons (startBtn, cancelBtn), ένα Listbox (listbox1) και ένα ProgressBar (progressBar1). Στο cancelBtn κάνουμε set το Enabled σε false, ώστε αρχικά να μην είναι δυνατό το “κλικ” σε αυτό. Οπότε, η φόρμα μας θα μοιάζει κάπως έτσι
Αφού κάνουμε διπλό κλικ στα startBtn και cancelBtn (ώστε να δημιουργηθούν οι απαραίτητοι event handlers), πάμε στον κώδικα της φόρμας
Καταρχάς, στο handler για το κλικ του startBtn γράφουμε τα εξής
Κάνουμε set σε null το datasource του listbox, ώστε να είναι τελείως άδειο.
Στη συνέχεια, κάνουμε enable και disable αντίστοιχα τα startBtn και cancelBtn. Στη συνέχεια, για να ξεκινήσει η χρονοβόρα διαδικασία, τρέχουμε τη μέθοδο RunWorkerAsync() στο backgroundWorker1.
Στον handler για το cancelBtn.Click γράφουμε τα εξής
με την μέθοδο CancelAsync ζητάμε ακύρωση της χρονοβόρας διαδικασίας.
Στη συνέχεια, στη μέθοδο backgroundWorker1_DoWork γράφουμε τα εξής
Η συγκεκριμένη μέθοδος καλείται όταν τρέξουμε τη μέθοδο RunWorkerAsync. Ας υποθέσουμε ότι η χρονοβόρα διαδικασία είναι η δημιουργία μερικών strings. Όταν αυτή τελειώσει, τότε τα δημιουργηθέντα strings θα εμφανιστούν στο listbox.
Να σημειωθεί ότι από αυτή τη συνάρτηση ΔΕΝ μπορούμε να πειράξουμε το user interface.
Αρχικά, γίνεται ένα new στο αντικείμενο e.Result. Αυτό είναι ένα αντικείμενο το οποίο είναι διαθέσιμο στους event handlers του backgroundWorker. Σε αυτό το αντικείμενο τοποθετούμε τη λίστα με τα strings που θα κατασκευάσουμε.
Το κάνουμε initialize σε List<string>, στη συνέχεια ξεκινάμε την επανάληψη, από 1 μέχρι 100 (έστω ότι θέλουμε 100 strings). Αρχικά, ελέγχουμε αν έχει γίνει αίτηση για cancellation του asynchronous task του background worker, και αν είναι έτσι, κάνουμε break από το loop. Ειδάλλως, δημιουργούμε ένα καινούριο Guid (επέλεγα αυτό τον τρόπο για τη δημιουργία τυχαίων strings), καλούμε την ToString() του, και το προσθέτουμε στην e.Result. Τέλος, καλούμε την ReportProgress, και της δίνουμε σαν παράμετρο το i.
Στη συνέχεια, παραθέτω τον κώδικα για τον ProgressChanged handler. Ναι, σωστά σκεφτήκατε! Το συγκεκριμένο event γίνεται raise όταν καλέσουμε την ReportProgress στο instance του BackgroundWorker.
Τι έχουμε εδώ; Από αυτή τη συνάρτηση, μπορούμε να πειράξουμε το user interface, και αυτό ακριβώς κάνουμε, κάνοντας set το value του progressbar. Τι τιμές παίρνει αυτό; Το κάθε progressbar control έχει δύο properties που αντιπροσωπεύουν τη minimum και τη maximum τιμή του, δηλαδή τα values που έχει το progressbar όταν η μπάρα είναι άδεια και όταν είναι γεμάτη, αντίστοιχα.
Όπως βλέπουμε, αυτά τα έχουμε αφήσει στις default τιμές τους, 0 και 100.
Το τελευταίο κομμάτι κώδικα που θα δείξουμε είναι το παρακάτω
Το συγκεκριμένο κομμάτι κώδικα εκτελείται όταν τερματίσει η εκτέλεση της DoWork, είτε αυτή ολοκληρώθηκε κανονικά, είτε διακόπηκε από αίτημα για ακύρωση. Βάλαμε ένα “εικονικό” if statement, ώστε να δείξουμε ότι υπάρχει ενημέρωση για το αν έγινε ακύρωση της DoWork ή κανονική ολοκλήρωση. Μετά από αυτό, απλά κάνουμε bind το e.Result (που περιέχει τη λίστα με τα strings μας) στο listbox, και κάνουμε enable και disable τα startBtn και cancelBtn, αντίστοιχα, ώστε αν θέλουμε να ξαναξεκινήσει η διαδικασία. Υπ’όψιν ότι, έτσι όπως είναι ο κώδικας, η εμφάνιση των strings στο listbox θα γίνει, είτε ακυρωθεί η DoWork είτε τερματίσει κανονικά.
Ο κώδικας για την εφαρμογή είναι συννημένος στο παρόν blog post.