Confessions of a .NET Developer!

Using BackgroundWorker in WPF Applications

In this post i shall explain a simple way for making your UI responsive while handling huge collection of data. For this, i found BackgroundWorker as the best solution. BackgroundWorker is under System.ComponentModel namespace.

The reason why i am using BackgroundWorker is because it is very easy to implement it. Not only that, it is completely thread safe as it takes an available thread from the 25 worker threads that is available in the Thread pool.
The BackgroundWorker also supports Cancellation of the current work, Support for progressbar.

In the example here i will take the case of loading images and displaying it to a ListBox.

Our window will look like this :

Window

Window

So our window will have a Browse button. This will use the FolderBrowserDialog to browse for images. On the right of the button is a progress bar which will show how much work has been done.

Lets first create our BackgroundWorker. This can either be done in XAML or in the code-behind, but i would rather prefer declaring it in the XAML. So here is the XAML declaring the BackgroundWorker.

        <cm:BackgroundWorker x:Key="backgroundWorker"
                             DoWork="BackgroundWorker_DoWork"
                             ProgressChanged="BackgroundWorker_ProgressChanged"
                             RunWorkerCompleted="BackgroundWorker_RunWorkerCompleted"
                             WorkerReportsProgress="True"
                             WorkerSupportsCancellation="True">
        </cm:BackgroundWorker>

You might be wondering what’s this cm?
You have to give a namespace to use the BackgroundWorker like below :

xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"

In simple terms remember one thing, The BackgroundWorker_DoWork will do the long running work and BackgroundWorker_RunWorkerCompleted is the place where the result from the Do_Work is updated in the UI. Also remember that in the DoWork, you cannot update the UI, you cannot access the UI Elements.

All these events be present in the code-behind.
Now to use this BackgroundWorker in our code-behind, do the below :

        private BackgroundWorker backgroundWorker;

        public Window3()
        {
            InitializeComponent();
            
            backgroundWorker = (BackgroundWorker)this.FindResource("backgroundWorker");
        }

The above code will find our backgroundworker defined in the Windows Resources in our XAML.

Next lets implement the simple code for our Browse button :

        private void btnBrowse_Click(object sender, RoutedEventArgs e)
        {
            FolderBrowserDialog openIt = new FolderBrowserDialog();
            //openIt.RootFolder = Environment.SpecialFolder.Desktop;
            openIt.RootFolder = Environment.SpecialFolder.MyComputer;
            DialogResult result = openIt.ShowDialog();
            if (result == System.Windows.Forms.DialogResult.OK)
            {
                txtBrowse.Text = openIt.SelectedPath;
            }
            //backgroundWorker.RunWorkerAsync(@"C:\Documents and Settings\singhta\Desktop");
        }

Now we are in a position to discuss how to show images in the Listbox.
In this example i am using DataBinding, so first i will show you the classes which i will be using.
Here is the FileInfoClass :

    class FileInfoClass
    {
        private String fileName;
        public String FileName
        {
            get { return fileName; }
            set { fileName = value; }
        }

        private BitmapImage btm;
        public BitmapImage Btm
        {
            get { return btm; }
            set { btm = value; }
        }

        private Decimal size;
        public Decimal Size
        {
            get { return size; }
            set { size = value; }
        }
        
    }

And this is our FileInfoCollection class :

class FileInfoCollection:ObservableCollection<FileInfoClass>
    {
    }

The above collection will store the collection of objects of type FileInfoClass.

Its time to implement the BackgroundWorker.
So when after selecting folder, the user will click the Load button .

Here is how its going to be implemented :

        private void btnLoad_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.RunWorkerAsync(txtBrowse.Text);
        }

        private void BackgroundWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
               if(backgroundWorker.CancellationPending)
                {
                     e.Cancel=true;
                     return;
                }
            String path = (String)e.Argument;
            e.Result = ShowImages(path);
        }

        private ObservableCollection<FileInfoClass> ShowImages(String path)
        {
            FileInfoCollection fic = new FileInfoCollection();

            DirectoryInfo di = new DirectoryInfo(path);
            IEnumerable<FileInfo> query = from FileInfo fi in di.GetFiles()
                                          where fi.Extension.ToUpper().Equals(".JPG") || fi.Extension.ToUpper().Equals(".PNG")
                                                || fi.Extension.ToUpper().Equals(".GIF")
                                          select fi;

            int i = 0;
            int totalItems = query.Count();
            foreach (FileInfo fi in query)
            {
                BitmapImage btm = new BitmapImage(new Uri(fi.FullName));
                btm.Freeze();
                fic.Add(new FileInfoClass()
                {
                    Btm = btm,
                    FileName = fi.Name,
                    Size = (fi.Length / 1024 / 1024)
                });
                backgroundWorker.ReportProgress((int)((100 / totalItems) * i));
                i = i + 1;
            }
            return fic;
        }

Don’t get worried! Its simple. So when Load button is clicked, we will use RunWorkerAsync method of the backgroundWorker which will call BackgroundWorker_DoWork function. RunWorkerAsync method will pass an argument containing the path. This path will be extracted using the e.Argument and subsequently passed to the ShowImages function which will recieve the path from where the images will be taken.

In the ShowImages function, i am using a LINQ query which will extract images of type jpg,png and gif. In the for loop we will create BitmapImage objects and assign it to the FileInfoClass. The freeze method is very important, or else you will get this exception :
“The calling thread cannot access this object because a different thread owns it.”

Next there is ReportProgress which will help us to provide values for our progress bar named progress.
Do remember one thing that to enable this feature, you must set ReportsProgress property to true(which i did in XAML itself).
Same goes for Cancellation too.
If you remember i have mentioned that BackgroundWorker provides an easy way to report how much progress has been made in progress bar.
The argument in ReportProgress takes value as %, it takes from 0 to 100.
The value in the ReportProgress arguement will be used in the below function :

        private void BackgroundWorker_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
        {
            progress.Value = e.ProgressPercentage;
        }

Remember that in the BackgroundWorker_DoWork function, you cannot use the controls defined in the form or else it will throw an exception. The ShowImages function will return FileInfoCollection object and assign it to the Result. This result will be used in the below function :

        private void BackgroundWorker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
        {
            if(e.Cancelled)
            {
                MessageBox.Show("Browsing Images has been cancelled");
            }
            progress.Value = 0;
            this.DataContext = e.Result as FileInfoCollection;
        }

So in the above function, we can access our controls. I have provided the DataContext to the window.

To cancel the work, use the CancelAsync method like below :

        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.CancelAsync();
        }

This is our Listbox in the XAML :

        <ListBox ItemsSource="{Binding}" Grid.Row="2" Margin="2" Name="lstImages"
                 ItemTemplate="{StaticResource BoxTemplate}"
                 ItemsPanel="{StaticResource BoxPanel}">           
        </ListBox>

The itemsource will take the DataContext defined above.
In the Window.Resources, i have declared the Itemtemplate and itemspanel for ListBox :

<Window.Resources>
        <DataTemplate x:Key="BoxTemplate">
            <Image Source="{Binding Path=Btm}" Stretch="Fill"/>
        </DataTemplate>
        
        <ItemsPanelTemplate x:Key="BoxPanel">
            <WrapPanel Orientation="Horizontal" ItemHeight="80" ItemWidth="80"/>
        </ItemsPanelTemplate>       
    </Window.Resources>

In the DataTemplate, i have specified the Image that will be displayed by Binding it to Btm property of the FileInfoClass class. I have used a WrapPanel as the ItemsPanel.

Now it should run smoothly as shown in the window above!

I will post the whole code-behind here :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media.Imaging;
using System.ComponentModel;
using System.IO;
using System.Collections.ObjectModel;
using System.Windows.Forms;

namespace PhotoViewer
{
    /// <summary>
    /// Interaction logic for Window3.xaml
    /// </summary>
    public partial class Window3 : Window
    {
        private BackgroundWorker backgroundWorker;

        public Window3()
        {
            InitializeComponent();
            
            backgroundWorker = (BackgroundWorker)this.FindResource("backgroundWorker");
        }

        private void btnBrowse_Click(object sender, RoutedEventArgs e)
        {
            FolderBrowserDialog openIt = new FolderBrowserDialog();
            //openIt.RootFolder = Environment.SpecialFolder.Desktop;
            openIt.RootFolder = Environment.SpecialFolder.MyComputer;
            DialogResult result = openIt.ShowDialog();
            if (result == System.Windows.Forms.DialogResult.OK)
            {
                txtBrowse.Text = openIt.SelectedPath;
            }
            //backgroundWorker.RunWorkerAsync(@"C:\Documents and Settings\singhta\Desktop");
        }

        private ObservableCollection<FileInfoClass> ShowImages(String path)
        {
            FileInfoCollection fic = new FileInfoCollection();

            DirectoryInfo di = new DirectoryInfo(path);
            IEnumerable<FileInfo> query = from FileInfo fi in di.GetFiles()
                                          where fi.Extension.ToUpper().Equals(".JPG") || fi.Extension.ToUpper().Equals(".PNG")
                                                || fi.Extension.ToUpper().Equals(".GIF")
                                          select fi;

            int i = 0;
            int totalItems = query.Count();
            foreach (FileInfo fi in query)
            {
                BitmapImage btm = new BitmapImage(new Uri(fi.FullName));
                //btm.Freeze();
                fic.Add(new FileInfoClass()
                {
                    Btm = btm,
                    FileName = fi.Name,
                    Size = (fi.Length / 1024 / 1024)
                });
                backgroundWorker.ReportProgress((int)((100 / totalItems) * i));
                i = i + 1;
            }
            return fic;
        }

        private void BackgroundWorker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            String path = (String)e.Argument;
            e.Result = ShowImages(path);
        }

        private void BackgroundWorker_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
        {
            progress.Value = e.ProgressPercentage;
        }

        private void BackgroundWorker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
        {
            progress.Value = 0;
            this.DataContext = e.Result as FileInfoCollection;
        }

        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.CancelAsync();
        }

        private void btnLoad_Click(object sender, RoutedEventArgs e)
        {
            backgroundWorker.RunWorkerAsync(txtBrowse.Text);
        }
    }
}

And the whole XAML here :

<Window x:Class="PhotoViewer.Window3"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
    Title="Window3" Height="204" Width="540">
    <Window.Resources>
        <cm:BackgroundWorker x:Key="backgroundWorker"
                             DoWork="BackgroundWorker_DoWork"
                             ProgressChanged="BackgroundWorker_ProgressChanged"
                             RunWorkerCompleted="BackgroundWorker_RunWorkerCompleted"
                             WorkerReportsProgress="True"
                             WorkerSupportsCancellation="True">
        </cm:BackgroundWorker>
        
        <DataTemplate x:Key="BoxTemplate">
            <Image Source="{Binding Path=Btm}" Stretch="Fill"/>
        </DataTemplate>
        
        <ItemsPanelTemplate x:Key="BoxPanel">
            <WrapPanel Orientation="Horizontal" ItemHeight="80" ItemWidth="80"/>
        </ItemsPanelTemplate>
        
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="30"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <DockPanel>
            <TextBox Name="txtBrowse" Width="200" Margin="2"/>
            <Button Name="btnBrowse" Click="btnBrowse_Click" Width="50" Margin="2" Content="Browse"/>
            <ProgressBar Name="progress" Margin="2"/>
        </DockPanel>
        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Margin="3" Width="60" Content="Load" Name="btnLoad" Click="btnLoad_Click" />
            <Button Margin="3" Width="60" Content="Cancel" Name="btnCancel" Click="btnCancel_Click" />
        </StackPanel>
        <ListBox ItemsSource="{Binding}" Grid.Row="2" Margin="2" Name="lstImages"
                 ItemTemplate="{StaticResource BoxTemplate}"
                 ItemsPanel="{StaticResource BoxPanel}">           
        </ListBox>
    </Grid>
</Window>

Please do check the FileInfoClass and FileInfoCollection classes that i have declared above.

Oh well that was it about BackgroundWorker.
Hope you enjoyed it!
Do drop in you feedback, comments and your valuable suggestions.
Cheers and happy coding! 🙂

Advertisements

March 14, 2011 - Posted by | WPF

1 Comment »

  1. This is a very clear and well-written article. Just the needed code and excellent explanations of what the code is doing. Bravo! …and THANKS!

    Comment by Darach | July 10, 2015 | Reply


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: