Mar 09 2010

Don’t shoot the messenger, don’t hang the UI!

Category: WPFMike Lovell @ 2:29 pm

I think everyone is guilty of this to an extent.  How often do  you execute code that ‘could’ take a long time to execute, under the UI thread?  The end-user presses a button, your code does it stuff, in the meantime the UI is, well, hung!  Sometimes it’s almost unnoticeable, a quick 500ms pause – But that code still shouldn’t be in there.  Because hangs can be minor, they will sometimes go unnoticed.

I thought I’d write a quick example of the right and wrong ways of dealing with code triggered from the UI thread.  At the bottom of the post is a Visual Studio 2010 Project which demonstrates this.  You will have to excuse my obsession with delegates!

First of all, I’m going to define a private ‘Thread’ which is going to handle my background work (Line 5):

   1:  // Author: Mike Lovell (mike.lovell@gotinker.com)
   2:   
   3:  public partial class MainWindow : Window
   4:  {
   5:      private Thread    workerThread;
   6:   
   7:   
   8:      public MainWindow()
   9:      {
  10:          InitializeComponent();
  11:      }
  12:   

In this WPF project, I have two buttons to demonstrate each method of doing things, called ‘buttonStartRight’ and ‘buttonStartWrong’.  Lets just make a little function to allow us to enable/disable them both at the same time

  13:   
  14:      private void SetButtonsIsEnabled(bool value)
  15:      {
  16:          buttonStartRight.IsEnabled    = value;
  17:          buttonStartWrong.IsEnabled    = value;
  18:      }
  19:   

Okay, now the wrong way to do it!

  20:   
  21:      private void buttonStartWrong_Click(object sender, RoutedEventArgs e)
  22:      {
  23:          SetButtonsIsEnabled(false);
  24:   
  25:          for (int i=0; i<100; i+= 10)
  26:          {
  27:              Thread.Sleep(1000);
  28:  
  29:              progress.Value = (double)i;
  30:          }
  31:   
  32:          SetButtonsIsEnabled(true);
  33:      }
  34:   

Although it may appear that this code will run nicely, it won’t (try it).  What will happen?  Nothing, until the entire function ’buttonStartWrong_Click’ has completed.  The progress bar WON’T be updated at each step, the buttons WON’T have ‘isEnabled’ set to false (from our function call on Line 21).

You’ll click the button, the UI thread will be blocked (the button won’t even pop up again indicating it’s clicked).  It’s a good old fashion hang (Ah, those VB6 days!).

So what is the right way?  Lets use that ‘Thread’ we declared earlier for this function:

  35:   
  36:      private void buttonStartRight_Click(object sender, RoutedEventArgs e)
  37:      {
  38:          SetButtonsIsEnabled(false);
  39:   
  40:          workerThread = new Thread(new ThreadStart(delegate()
  41:              {
  42:                  for (int i=0; i<100; i+= 10)
  43:                  {
  44:                      Thread.Sleep(1000);
  45:  
  46:                      Dispatcher.BeginInvoke((MethodInvoker)delegate()
  47:                          {
  48:                              progress.Value = (double)i;
  49:                          }, null);
  50:                  }
  51:   
  52:                  Dispatcher.BeginInvoke((MethodInvoker)delegate()
  53:                      {
  54:                          SetButtonsIsEnabled(true);
  55:                      }, null);
  56:              }));
  57:   
  58:          workerThread.Start();
  59:      }
  60:  }

So I’ve created an instance of a‘Thread’ with an anonymous delegate containing the code I want to run (Line 40).  As I’m now in a different thread from the UI, I won’t be allowed to directly work with its controls.  I need to use the ‘Dispatcher’ to talk to the UI thread.  I’ve made anonymous delegates for each of my calls back to the UI thread to make updates (to both the progress bar on Line 46, and the buttons on line 52). 

Et Voila

Some other bloggers covering multi-threading in WPF:

Download Visual Studio 2010 Project (13.5k)

Tags: , , , , , ,