Som udgangspunkt er en Windows Forms applikation singlethreaded. Foretager applikationen en længerevarende beregning/filindlæsning
eller lignende algoritme "fryser" brugergrænsefladen så længe arbejdet er i gang. Det levner heller ikke mulighed for at afbryde
algoritmen. Tilføjes en Cancel knap, så kan brugeren godt trykke på den, men cancel-trykket opfattes først efter algoritmen er afsluttet og
så har det jo ikke den store effekt :-)
I sådan en situation er asynkron afvikling af den længerevarende beregning løsningen. Læs mere om multithreading og hvorledes dette
problem kan løses i .NET 1.1 - se foredrag og demo i artiklen:
Microsoft Techdays 2003 i Odense den 21. og 22. maj under overskriften: "Bedre brugergrænseflader med multithreading",
Download Præsentation og Download demo - Generel progress håndtering.
I .NET 2.0 er det blevet lettere at foretage asynkron afvikling i en Windows Forms applikation - løsningen er BackgroundWorker.
System.ComponentModel.BackgroundWorker
Træk en BackgroundWorker fra Toolbox'en ind på design-viewet af Form'en og halvdelen af arbejdet er gjort...
Det centrale i BackgroundWorker er dens 3 events: DoWork, ProgressChanged og RunWorkerCompleted. Eventet DoWork
kaldes asynkront, så det er her vi skal afvikle den længerervarende proces. Da afviklingen foregår asynkront (i sin egen tråd) kan
brugeren arbejde videre i applikationen mens processen foregår.
Angivelse af fremskridt
Skal brugeren informeres om fremskridtet af den asynkrone proces, så kan man i den asynkrone metode kalde ReportProgress med
en procent angivelse af hvor langt processen er nået. Dette kald resulterer i at eventet ProgressChanged kaldes, men det vigtige her
er at ProgressChanged ikke kaldes direkte af DoWork, men at ProgressChanged automatisk afvikles af hovedtråden.
For at aktivere ProgressChanged eventet skal propertien WorkerReportsProgress sættes til true.
Resultatformidling
Er der et resultat af den længerevarende process kan det formidles gennem RunWorkerCompleted. Dette event kaldes når
DoWork afsluttes. På samme vis som ReportProgress kaldes RunWorkerCompleted også af hovedtråden - og hvorfor er dette så
væsentligt? På dette retoriske spørgsmål er svaret: Windows Forms kontroller
er som udgangspunkt kun beregnet til singlethreaded afvikling. Derfor kan der opstå multithreading fejl i kontrollerne, hvis de kaldes fra '
den asynkrone proces. Ved kun at opdater brugergrænseflade kontroller igennem kode, der afvikles på hoved tråden så foregår alt gennem
én tråd og alt er således som det skal og bør være.
Fortryd undervejs
BackgroundWorker understøtter også at processen kan afbrydes undervejs. Dette gøres ved at kalde CancelAsync, f.eks. ved tryk på en
Cancel knap. Dette afbryder ikke direkte den asynkrone process, vi skal selv kode afbrydelsen ved i DoWork metoden med jævne
mellemrum at teste propertien: CancellationPending. Er den sat til true så skal vi afbryde den igangværende proces.
For at aktivere muligheden for at afbryde, så skal WorkerSupportsCancellation sættes til true.
Eksempel
Som illustration er her en simpel Form. Når den længerervarende proces igangsættes vises en progressbar og en Cancel knap på formen.
Når den længerevarende proces afsluttes så sættes et resultat i DoWork, der kan aflæses i RunWorkerCompleted:
Afbrydes processen undervejs, så rulles progress baglæns, dette blot for at illustrere muligheden for at man også kan vise
fremskridtet af en oprydning, som det kendes fra f.eks. nogle Windows Installationsprogrammer.
partial class Form1 : System.Windows.Forms.Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, System.EventArgs e)
{
btnStart.Enabled = false;
btnCancel.Text = "Cancel";
btnCancel.Enabled = true;
panelProgress.Visible = true;
progressBar1.Value = 0;
backgroundWorker1.RunWorkerAsync();
}
private void backgroundWorker1_DoWork(
object sender, System.ComponentModel.DoWorkEventArgs e)
{
int i;
for (i = 0; i < 100; i++)
{
if (backgroundWorker1.CancellationPending)
break;
System.Threading.Thread.Sleep(50);
backgroundWorker1.ReportProgress(i);
}
if (i < 100)
{
for (; i > 0; i--)
{
System.Threading.Thread.Sleep(50);
backgroundWorker1.ReportProgress(i);
}
e.Result = "Afbrudt af bruger";
}
else
e.Result = "Sucess";
}
private void backgroundWorker1_ProgressChanged(
object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
private void backgroundWorker1_RunWorkerCompleted(
object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
lblResult.Text = (string)e.Result;
panelProgress.Visible = false;
btnStart.Enabled = true;
}
private void btnCancel_Click(object sender, System.EventArgs e)
{
backgroundWorker1.CancelAsync();
btnCancel.Text = "Canceling";
btnCancel.Enabled = false;
}
}
Public Class Form1
Private Sub button1_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnStart.Click
btnStart.Enabled = False
btnCancel.Text = "Cancel"
btnCancel.Enabled = True
panelProgress.Visible = True
progressBar1.Value = 0
backgroundWorker1.RunWorkerAsync()
End Sub
Private Sub backgroundWorker1_DoWork( _
ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles backgroundWorker1.DoWork
Dim i As Integer
For i = 0 To 100
If (backgroundWorker1.CancellationPending) Then
Exit For
End If
System.Threading.Thread.Sleep(50)
backgroundWorker1.ReportProgress(i)
Next
If (i < 100) Then
While i > 0
System.Threading.Thread.Sleep(50)
backgroundWorker1.ReportProgress(i)
i -= 1
End While
e.Result = "Afbrudt af bruger"
Else
e.Result = "Sucess"
End If
End Sub
Private Sub backgroundWorker1_ProgressChanged( _
ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
Handles backgroundWorker1.ProgressChanged
progressBar1.Value = e.ProgressPercentage
End Sub
Private Sub backgroundWorker1_RunWorkerCompleted( _
ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles backgroundWorker1.RunWorkerCompleted
lblResult.Text = CStr(e.Result)
panelProgress.Visible = False
btnStart.Enabled = True
End Sub
Private Sub btnCancel_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnCancel.Click
backgroundWorker1.CancelAsync()
btnCancel.Text = "Canceling"
btnCancel.Enabled = False
End Sub
End Class
|