Som bekendt tager ting tid – og mange og store ting tager lang tid. Så selv om Windows Forms baserede applikationer som regel performer godt, findes der absolut tilfælde, hvor ting set fra brugerens synspunkt tager for lang tid med det resultat, at man ikke kan interagere med applikation – applikationen er ikke responsiv.
Eksempler på operationer, der tager lang tid, kan være langvarige database-operationer, mangfoldige disk operationer som f.eks. traversering af en større filstruktur, eller måske mange på hinanden følgende XML web service kald. Også et enkeltstående XML web service kald kan sagtens tage lang tid, hvis for eksempel kommunikationen sker over en langsom linje, mængden af de transmitterede data er stor eller der er tale om en kompleks operation. Selv almindelige matematiske beregninger kan i nogle tilfælde være af et omfang, så ventetiden føles ulidelig for brugeren.
Hvorvidt man kan gøre noget reelt for at forbedre brugeroplevelsen afhænger i høj grad af den konkrete applikation. Allerbedst er det naturligvis, hvis man kan strikke sin applikation sammen på en sådan måde, at brugeren kan fortsætte med at bruge applikationen, mens den langvarige operation snøvler sig færdig. Desværre er det ikke i alle scenarier, at det giver specielt meget mening at fortsætte brugen af applikationen helt upåagtet – og selv om det måtte give mening, kan det sagtens være en kompliceret affære at implementere en sådan funktionalitet. Generelt føles ventetiden for brugeren dog som mindre problematisk, hvis der kommer en vis form for feedback under eksekveringen af applikationen – feedback i form af eksempelvis opdatering af en progressbar eller lignende.
Multithreading og Windows Forms
I .NET findes der en glimrende understøttelse af multithreading, som gør, at man meget fleksibelt kan implementere Windows applikationer med såvel feedback som egentlig understøttelse af interaktivitet under eksekveringen af langvarige operationer. En yderligere ganske vigtig, om end kosmetisk fordel, man får af at kalde operationer asynkront, er, at brugergrænsefladen fortsat vil kunne gentegne sig selv.
Desværre risikerer man ved implementation af asynkron funktionalitet i Windows applikationer hurtigt at løbe ind i, er, at det ikke er sikkert at manipulere Windows kontroller fra en anden tråd end den, der ejer det underliggende window handle. Manipulation af en Windows kontrol bør derfor ske ved at anvende kontrollens Invoke-metode til at få kaldt en delegate på den tråd, der ejer kontrollens window handle (i det følgende vil vi kalde denne tråd for user interface tråden).
Denne metode er i sig selv glimrende – og meget generel anvendelig - men af mange også anset for at være en anelse omstændelig.
BackgroundWorker komponenten
BackgroundWorker-komponenten kan opfattes som en implementation af et designpattern, der har til formål at forsimple user interface integrationskode i et multithreadet eksekveringsscenarie. BackgroundWorker-komponenten er på ingen måde en afløser for hverken multithreading generelt set eller mere specifikt for kald af Invoke-metoden på user interface kontroller – dens eneste formål er at forsimple nogle hyppigt forekommende scenarier.
BackgroundWorker-komponenten kan placeres på en Windows form ved blot at hive den ind fra toolboxen, og den kan herefter anvendes direkte i ens kode.
Figur: Overblik over BackgroundWorker-komponentens funktionalitet.
DoWork-eventet
Funktionaliteten i BackgroundWorker er bygget op omkring affyringen/genereringen af en række events, hvoraf det mest centrale er DoWork-eventet:
Public Event DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
public delegate void DoWorkEventHandler(object sender, DoWorkEventArgs e)
public event DoWorkEventHandler DoWork
Den applikationskode der skal eksekveres asynkront placeres i DoWork-eventproceduren, idet denne eventprocedure bliver kaldt på en ny selvstændig tråd, hvilket frigør user interface tråden til at kunne interagere med brugeren. Så det er altså i denne eventprocedure, at man eksempelvis kan traversere filsystemet. For at undgå problemer må man endelig ikke lave direkte manipulation af kontroller fra DoWork-eventproceduren.
RunWorkerAsync-metoden
DoWork-eventproceduren bliver kaldt, når klientkoden kalder RunWorkerAsync-metoden på BackgroundWorker-komponenten, hvilket typisk sker fra kode i user interface tråden – for eksempel fra en knaps klik-event. RunWorkerAsync-metoden findes i to overloadede versioner: med og uden en argument-parameter. Denne argument-parameter bliver overført til Argument-propertyen på DoWork-eventets DoWorkEventArgs-parameter, hvorved man har mulighed for at overføre de nødvendige parametre til eksekveringen af DoWork-eventproceduren.
DoWork-eventproceduren kaldes asynkront, og brugeren kan derfor umiddelbart fortsætte med at interagere med applikationens brugergrænseflade – dog med en enkelt undtagelse: En given instans af BackgroundWorker-komponenten kan kun håndtere ét asynkront kald ad gangen, og det er derfor en god ide at forhindre simultane eksekveringer ved eksempelvis at disable den knap, som kalder RunWorkerAsync-metoden.
ProgressChanged-eventet
Som sagt er det en rigtig god ide at give brugeren feedback under eksekveringen af et langvarigt job, og ofte er en progressbar en oplagt måde at gøre det på. For at vi kan få lov til at manipulere eksempelvis en progressbar-kontrol indefra DoWork-eventproceduren, stiller BackgroundWorker-komponenten et ProgressChanged-event til rådighed:
Public Event ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs)
public delegate void ProgressChangedEventHandler(object sender, ProgressChangedEventArgs e)
public event ProgressChangedEventHandler ProgressChanged
Dette event kan vi få kaldt ved at kalde ReportProgress-metoden på BackgroundWorker-komponenten fra DoWork-eventproceduren. ReportProgress-metoden findes i to overloadede versioner, der stiller parametre til rådighed for ProgressChanged-eventet for derved at lette overførslen af data til brug for eventuel feedback til brugeren:
Public Sub ReportProgress(ByVal percentProgress As Integer)
Public Sub ReportProgress(ByVal percentProgress As Integer, ByVal userState As Object)
public void ReportProgress(int percentProgress)
public void ReportProgress(int percentProgress, object userState)
percentProgress-parameteren kan, som navnet antyder, bruges til at angive, hvor langt processen er nået, men der er naturligvis ingen krav om, at percentProgress rent faktisk angiver en procentsats. userState-parameteren kan om ønsket bruges til at overføre yderligere applikationsspecifikke informationer så som en statustekst eller lignende. Disse parametre dukker derefter under navnene ProgressPercentage og UserState op som properties på ProgressChangedEventArgs-parameteren til ProgressChanged-eventet.
ProgressChanged-eventet bliver kaldt på user interface tråden, og vi kan derfor uden risiko manipulere kontroller fra denne. ProgressChanged-event bliver kun genereret, hvis WorkerReportsProgress-propertyen på BackgroundWorker-komponenten er sat til True.
For ikke at give et unødvendigt overhead i eksekveringen, er det bedst kun at kalde WorkerReportsProgress-metoden, når der rent faktisk er grund til det; det vil sige i de tilfælde, hvor der rent faktisk skal ske en manipulation af user interface kontroller.
RunWorkerCompleted-eventet
BackgroundWorker-komponenten stiller endnu et event til rådighed:
Public Event RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
public delegate void RunWorkerCompletedEventHandler(object sender, RunWorkerCompletedEventArgs e)
public event RunWorkerCompletedEventHandler RunWorkerCompleted
Som navnet antyder, genereres dette event, når DoWork-metoden afsluttes.
RunWorkerCompletedEventArgs-parameteren har blandt andet en Result-property:
Public ReadOnly Property Result() As Object
public object Result { get; }
Result-propertyen kan i DoWork-eventproceduren sættes på DoWorkEventArgs-parameteren og altså efterfølgende aflæses på RunWorkerCompletedEventArgs-parameteren i RunWorkerCompleted-eventproceduren. Hvis det asynkront eksekverede og langvarige job har et egentligt resultat, er det naturligt at overføre dette til user interface tråden via Result-propertyen.
Brug af BackgroundWorker-komponenten step-by-step
Som en opsummering af ovenstående er der her en lille step-by-step opskrift til, hvordan man bruger BackgroundWorker-komponenten.
- Træk BackgroundWorker-komponenten ind på en Windows form.
- Sæt WorkerReportsProgress-propertyen til True.
- Fra eksempelvis en knaps Click-event kaldes RunWorkerAsync-metoden. Medsend eventuelt en argument-parameter.
- Implementér den kode der skal eksekveres asynkront i DoWork-eventproceduren.
- Kald ReportProgress-metoden passende steder i DoWork-eventproceduren.
- Returner et eventuelt resultat af DoWork-eventprocedurens eksekvering via Result-propertyen på DoWorkEventArgs-parameteren
- Implementér den kode, der skal opdatere user interfacet i ProgressChanged-eventproceduren.
- Implementér RunWorkerCompleted-eventproceduren.
Implementation af Cancel-funktionalitet
Da man som bruger ind imellem fortryder, at man har sat et langvarigt job i gang, er der i nogle situationer god ræson i, at brugeren kan afbryde operationen. Det skal dog bemærkes, at det ikke altid er helt trivielt at stille en sådan funktionalitet til rådighed: Hvilke konsekvenser har det for eksempel, at man afbryder kopieringen af en række filer? Skal man slette de allerede kopierede filer eller blot brutalt afbryde kopieringen på det sted, man måtte være kommet til?
Skulle man være så venlig en udvikler-sjæl, at man rent faktisk beslutter sig for at tilbyde brugerne mulighed for at afbryde det asynkrone job, kan BackgroundWorker-komponenten også hjælpe i den situation, idet den stiller en CancelAsync-metode til rådighed for klienten. For at kunne kalde denne metode skal man dog først sætte WorkerSupportsCancellation-propertyen på BackgroundWorker-komponenten til True.
Når CancelAsync-metoden så kaldes, bliver BackgroundWorker-komponentens CancellationPending-property sat til True, og efter test af denne værdi kan DoWork-eventproceduren eventuelt beslutte sig for at afbryde sit langvarige job. Hvis den vælger at gøre dette, bør den samtidig sætte Cancel-propertyen på DoWorkEventArgs-parameteren til True. Dermed kan RunWorkerCompleted-eventproceduren ved aflæsning af RunWorkerCompletedEventArgs-parameterens Cancelled-property konstatere, at grunden til, at den langvarige proces er afsluttet, er, at brugeren (og efterfølgende DoWork-eventproceduren) har afbrudt jobbet. Det er vigtigt, at RunWorkerCompleted-eventproceduren checker, at Cancelled-propertyen er False, inden Result-propertyen aflæses, idet Result-propertyen ellers vil kaste en System.Reflection.TargetInvocationException.
Implementation af Cancel-funktionalitet step-by-step
Som en opsummering er der her en lille step-by-step opskrift til, hvordan man implementerer cancel-funktionalitet.
- Sæt WorkerSupportsCancellation-propertyen til True.
- Kald CancelAsync-metoden fra klientkoden (for eksempel fra en knaps klik-event).
- Test på CancellationPending-propertyen i DoWork-eventproceduren. Sæt Cancel-propertyen på DoWorkEventArgs-parameteren til True. Afslut eksekveringen i DoWork-metoden.
- Test på Cancelled-propertyen i RunWorkerCompleted-eventproceduren.
Simpelt gør hvad simpelt er
Nu skal der nok være de, der ikke synes, at BackgroundWorker-komponenten har gjort håndteringen af asynkron eksekvering det mindste simplere - det har jo føget rundt med metoder, events, eventargs-parametre og properties på samme, og det er da også ganske rigtigt, at der er meget at holde rede på, når man skal i gang med at anvende BackgroundWorker-komponenten for første gang. Til gengæld får man uden videre kodemæssig omkostninger stillet en række standardfunktionaliteter til rådighed - standardfunktionaliteter som man naturligvis ikke er tvunget til at gøre brug af, men som kan være med til at forsimple ens kode, den dag man rent faktisk har behov for dem.
Med en lettere omskrivning af en klassisk talemåde kan man sige at ”bag ethvert responsivt user interface står en BackgroundWorker”. Ligesom i original-udgaven er det ganske vist ikke en talemåde, der er helt korrekt, da man sagtens kan lave responsive user interfaces uden brug af BackgroundWorker-komponenten; og en del udviklere vil da givet vis også foretrække at styre det hele selv ved hjælp af Invoke-metoden.
BackgroundWorker-komponenten løser langt fra alle multithreading problemstillinger. Til gengæld er der rigtigt mange udviklere, der synes, at BackgroundWorker-komponenten efter lidt tilvænning gør implementations-arbejdet med asynkrone jobs i Windows applikationer en hel del simplere, og det er vel i sig selv heller ikke så ringe endda.
Der følger et lille kodeeksempel med til denne artikel. Eksemplet findes både til VB og C#, og det kan downloades her.