Monday, 21 July 2008

WPF Splash with ProgressBar

There are some excellent articles on Quantum Bit Designs on WPF and cross-thread updates. I borrowed one of them to implement a splash screen in my WPF demo application.

On start up of my WPF GUI application, I want to show a splash window with a progress bar underneath. At the same time, I do some potentially time-consuming tasks, such as openning a database connection and load some reference data into memory. Since the duration of this type of operation cannot be known up front and the percentage of each step wrt the entire operation cannot be determined either, it is sensible to use an undeterministic progress bar. The SplashScreen.xaml file is shown below:


    
        
            
            
        
    

The trick is to have a System.ComponentModel.BackgroundWorker to perform the long running operation. The initialisation operation class is defined as such:

public class InitOperation {
        bool completed = false;
        string errorDescription;
        /// 
        /// the operation is either completed successfully (true) or
        /// aborted due to exceptions (false).
        /// 
        public bool Completed {
            get { return completed; }
        }
        /// 
        /// the error text to be displayed in GUI in case of error
        /// (i.e. completed==false)
        /// 
        public string ErrorDescription {
            get { return errorDescription; }
        }

        public InitOperation() {
            completed = false;
            errorDescription = String.Empty;
        }

        public void Start() {
            BackgroundWorker worker = new BackgroundWorker();
            worker.DoWork+=new DoWorkEventHandler(worker_DoWork);
            worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
            worker.RunWorkerAsync();
        }

        void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            OnComplete(EventArgs.Empty);
        }
        /// 
        /// Upon operation completion, fire a Complete event so that the 
        /// observer can receive and act on it.
        /// 
        protected virtual void OnComplete(EventArgs e) {
            if (this.Complete != null) {
                this.Complete(this, e);
            }
        }
        /// 
        /// The long-running operation is performed here.
        /// 
        void worker_DoWork(object sender, DoWorkEventArgs e) {
            try {
                // long-running operation goes here.
                completed=true;
            } catch (Exception ex) {
                completed=false;
                errorDescription = ex.Message + "\n" + ex.StackTrace;
            }
        }
        /// 
        /// the event to be fired upon operation completion
        /// 
        public event EventHandler Complete;
    }

The core of the InitOperation is in the Start() method. It creates a background worker, adds the event handlers and then executes the worker in another thread. Once the worker completes, it will fire a Complete event. Whoever is listening to the event will receive it. So I need to have the Splash screen (running in the GUI thread) to register to the event and handle it:

public partial class SplashScreen : Window {
        InitOperation _operation;

        public SplashScreen(InitOperation worker) {
            this._operation = worker;
            this._operation.Complete += new EventHandler(_worker_Complete);
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(Splash_Loaded);
        }

        void _worker_Complete(object sender, EventArgs e) {
            if (_operation.Completed) {
                MainWindow mw = new MainWindow();
                Close();
                mw.Show();
            }
            else {
                MessageBox.Show("Cannot initialise application:\n"+_operation.ErrorDescription,
                "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Error);
                Close();
            }
        }

        void Splash_Loaded(object sender, RoutedEventArgs e) {
            _operation.Start();
        }
    }

The SplashScreen class is very straight forward. It registers itself to the InitOperation's Complete event and then Start() the initialisation operation. It takes an InitOperation as input parameter for its constructor. This wiring up is done at the App.xaml and App.xaml.cs.


 
  
   
    
   
  
 

Instead of specifying the StartupUri, a Startup event handler is defined in App.

public partial class App: System.Windows.Application
 {
        private void Application_Startup(object sender, StartupEventArgs e) {
            SplashScreen splash = new SplashScreen(new InitOperation());
            splash.Show();
        }
 }

5 comments:

Jonathan Fernández said...

Hi!!

I tried to implement a splash screen following your intructions.

I created a test aplication, and obviously, the splash screen was too fast to apreciate it.

First i put a thread.sleep() in the constructor of the main window to get more time. All works fine but the progress bar don't move.

Then I deleted the sleep and write a while(true); but also didn't work. The Progress Bar don't move.
¿Any idea?

Thanks ;)

Jonathan Fernández said...

Ok,
I know that I should move the hard work of my program to worker_DoWork() function, not in the Main Window Constructor.

Dante123 said...

thank you so much for this post,

but i really don't understand 1 thing:

if i want to fake a longer start, i can just put Thread.Sleep(1000); in the DoWork.

but then the progressbar wont visually increase.

so how can i do that ?

thank you very much in advance!

sammy_winter said...

When you do a Thread.Sleep, you are blocking the thread you are working with, which will be the UI thread. Because you are doing this on the UI Thread, it is not available to update the progress bar.

.NET 4.5 has a keyword await to allow you to give up control of your current thread and resume later, so you could say:
await thread.Sleep(1000);
this should give you the desired effect.

Omotayo Jacob said...

Where is the Simple Styles.xaml markup and the .GIF image that the image source points to