Julekalender 2004 om Whidbey


20. dec 2004 00:10

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)
  {
    // This method will run on a thread other than the UI thread.
    // Be sure not to manipulate any Windows Forms controls created
    // on the UI thread from this method.

    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
    'This method will run on a thread other than the UI thread.
    'Be sure not to manipulate any Windows Forms controls created
    'on the UI thread from this method.

    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


Abonnér på mit RSS feed.   Læs også de øvrige indlæg i denne Blog.