Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF WAIT PROGRESS BAR USING THREADING

0.00/5 (No votes)
3 Apr 2010 1  
WPF Wait Progress Bar using threading in an XBAP application

Introduction

I am now working on a project which involves creation of a WPF / XBAP full trust application for my employer. One of the tasks which took some time for me, was to find out, how to display a wait progress bar.

Background

I noticed a very good article on the subject at http://sachabarber.net/?p=639 – however I very soon found out that the circular progress bar animation takes effect only when you are making asynchronous calls to a web service – many of our time consuming tasks are through web service calls – so initially I thought that this should suffice. Then we started finding other long, time consuming tasks creeping into our UI – in such cases when we use the circular progress bar, the animation won’t happen as WPF gives preference to the tasks happening in the UI thread – only on their completion would the UI be updated and the animation will happen.

Then I did some reading up on threading and came up with a solution where I can use the circular progress bar in the UI thread scenarios too by using the Dispatcher.Invoke methods to update a standard ProgressBar. The circular progress bar looks like this:

WPFProgressBar-ScreenShot.jpg

When the standard progress bar is being updated, the UI thread also does the animation for the circular progress bar before it jumps back into the other tasks in the UI process. For the user the animation seems to happen as usual (the time delay is not very noticeable) – if the steps in your UI process are of equal length then the animation happens really smoothly. If they are of unequal length, the animation still happens (of course in a jerky fashion which though is not very noticeable).

The zip file link (Download WpfBrowserApplication2.zip - 186.17 KB) at the top of this article, contains a Visual Studio 2008 WPF / XBAP application in VB.Net. In this example I have tried out the following scenarios:

  • Counting from 1 to 32767 – here the steps are of equal length – so the animation will be really smooth.
  • Prime number calculation – same as above – however I am not updating the standard progress bar – as I don’t know the maximum value, it is set to zero. The circular wait-progress bar updates smoothly as the steps are of equal length.
  • Long multi-step UI thread process – here I am simulating a 30 step UI thread process with unequal intervals – you will notice that the animation becomes smoother nearing completion as the time intervals become shorter.

I have not included calling a web service asynchronously in my example – however I have tested this – using the WPF progress bar with a webservice involves adding a handler to the completed event method of your webservice, starting the progress bar animation, calling the webservice method asynchronously and then when the completed event method fires, stopping the animation. Here again the animation will be very smooth as the UI thread is idle waiting for the return value from the webservice completed event – however as we don’t know the maximum value we have to set it to zero similar to the prime number calculation.

The CircularProgressBar xaml goes as follows:

<UserControl x:Class="CircularProgressBar"
    xmlns="%22http://schemas.microsoft.com/winfx/2006/xaml/presentation%22">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="%22http://schemas.microsoft.com/winfx/2006/xaml%22">http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfBrowserApplication2"
    Height="Auto" 
    Width="Auto" 
    Background="Transparent" 
    IsVisibleChanged="HandleVisibleChanged"
    >
    <Grid x:Name="LayoutRoot" 
          Background="Transparent"
          ToolTip="Searching...."
          HorizontalAlignment="Center"
          VerticalAlignment="Center"
          DataContext="{x:Static local:SessionHandler.StartUpLoadData}" 
          >
        <Canvas 
            RenderTransformOrigin="0.5,0.5"
            HorizontalAlignment="Center"
            VerticalAlignment="Center" 
            Width="120"
            Height="120" 
            Loaded="HandleLoaded"
            Unloaded="HandleUnloaded"  
            >
            <Ellipse x:Name="C0" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="1.0">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C1" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.9">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C2" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.8">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C3" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.7">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C4" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.6">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C5" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.5">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C6" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.4">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C7" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.3">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Ellipse x:Name="C8" Width="20" Height="20"
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill"
                     Opacity="0.2">
                <Ellipse.Fill>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </Ellipse.Fill>
            </Ellipse>
            <Canvas.RenderTransform>
                <RotateTransform x:Name="SpinnerRotate"
                     Angle="0" />
            </Canvas.RenderTransform>
        </Canvas>
    </Grid>
</UserControl>

The CircularProgressBar code behind goes as follows:

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Windows.Input
Imports System.Windows.Shapes
''' <summary>
''' A circular type progress bar, that is simliar to popular web based
''' progress bars
''' </summary>
Partial Public Class CircularProgressBar
#Region "Data"
    Private ReadOnly animationTimer As DispatcherTimer
#End Region
#Region "Constructor"
    Public Sub New()
        InitializeComponent()
        animationTimer = New DispatcherTimer(DispatcherPriority.Background, Dispatcher)
        'animationTimer.Interval = New TimeSpan(0, 0, 0, 0, 75)
        animationTimer.Interval = New TimeSpan(0, 0, 0, 0, 300)
    End Sub
#End Region
#Region "Private Methods"
    Private Sub Start()
        Mouse.OverrideCursor = Cursors.Wait
        AddHandler animationTimer.Tick, AddressOf HandleAnimationTick
        animationTimer.Start()
    End Sub
    Private Sub [Stop]()
        animationTimer.[Stop]()
        Mouse.OverrideCursor = Cursors.Arrow
        RemoveHandler animationTimer.Tick, AddressOf HandleAnimationTick
    End Sub
    Private Sub HandleAnimationTick(ByVal sender As Object, ByVal e As EventArgs)
        SpinnerRotate.Angle = (SpinnerRotate.Angle + 36) Mod 360
    End Sub
    Private Sub HandleLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Const offset As Double = Math.PI
        Const [step] As Double = Math.PI * 2 / 10.0R
        SetPosition(C0, offset, 0.0R, [step])
        SetPosition(C1, offset, 1.0R, [step])
        SetPosition(C2, offset, 2.0R, [step])
        SetPosition(C3, offset, 3.0R, [step])
        SetPosition(C4, offset, 4.0R, [step])
        SetPosition(C5, offset, 5.0R, [step])
        SetPosition(C6, offset, 6.0R, [step])
        SetPosition(C7, offset, 7.0R, [step])
        SetPosition(C8, offset, 8.0R, [step])
    End Sub
    Private Sub SetPosition(ByVal ellipse As Ellipse, ByVal offset As Double, ByVal posOffSet As Double, ByVal [step] As Double)
        ellipse.SetValue(Canvas.LeftProperty, 50.0R + Math.Sin(offset + posOffSet * [step]) * 50.0R)
        ellipse.SetValue(Canvas.TopProperty, 50 + Math.Cos(offset + posOffSet * [step]) * 50.0R)
    End Sub
    Private Sub HandleUnloaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
        [Stop]()
    End Sub
    Private Sub HandleVisibleChanged(ByVal sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
        Dim isVisible As Boolean = CBool(e.NewValue)
        If isVisible Then
            Start()
        Else
            [Stop]()
        End If
    End Sub
#End Region
End Class
   

The main page is Page1. The xaml in this file is as follows:

<Page x:Class="Page1"
    xmlns="%22http://schemas.microsoft.com/winfx/2006/xaml/presentation%22">http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="%22http://schemas.microsoft.com/winfx/2006/xaml%22">http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfBrowserApplication2"
      
    Title="Page1" >
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Button Grid.Row="0" Grid.Column="0" Name="Button1" Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" >Click me to count from 1 to 32767</Button>
        <Label Grid.Row="0" Grid.Column="1"  Name="Label1" Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" />
        <StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"  Orientation="Horizontal" VerticalAlignment="Center" >
            <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="2,2,2,2"
            />
            <TextBlock Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" >Biggest Prime Found:</TextBlock>
            <TextBlock Name="bigPrime" Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" >3</TextBlock>
        </StackPanel>
        <StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"  Orientation="Horizontal" VerticalAlignment="Center" >
            <Button Content="Start long multi-step UI thread process"  
            Click="MultiStepUIProcess"
            Name="MultiUIButton"
            Margin="2,2,2,2"
            />
            <TextBlock Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" >Last step completed:</TextBlock>
            <TextBlock Name="stepText" Margin="2,2,2,2" HorizontalAlignment="Left" VerticalAlignment="Center" ></TextBlock>
        </StackPanel>
        <Viewbox Grid.Row="0" Grid.Column="0" Grid.RowSpan="4" Grid.ColumnSpan="2" x:Name="MyViewBox" DataContext="{x:Static local:SessionHandler.StartUpLoadData}" >
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <local:CircularProgressBar x:Name="MyProgressBar" Grid.Row="0" >
                <local:CircularProgressBar.Visibility>
                    <Binding Path="TimerIsVisible" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" Converter="{local:BooleanToVisibilityConverter}" >
                    </Binding>
                </local:CircularProgressBar.Visibility>
            </local:CircularProgressBar>
            <ProgressBar Grid.Row="1" Height="10" Name="ProgressBar1" VerticalAlignment="Bottom" ValueChanged="ProgressBar1_ValueChanged" DataContext="{x:Static local:SessionHandler.StartUpLoadData}" >
                <ProgressBar.Background>
                    <Binding Path="CircularProgressBarFill" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" >
                    </Binding>
                </ProgressBar.Background>
            </ProgressBar>
            </Grid>
            <Viewbox.Visibility>
                <Binding Path="TimerIsVisible" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" Converter="{local:BooleanToVisibilityConverter}" >
                </Binding>
            </Viewbox.Visibility>
        </Viewbox>
    </Grid>
</Page>

The code behind of this file is as follows:

Imports System
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Threading
Imports System.Threading
Class Page1
#Region "Private variables"
    'Create a Delegate that matches the Signature of the ProgressBar's SetValue method
    Private Delegate Sub UpdateProgressBarDelegate(ByVal dp As System.Windows.DependencyProperty, _
                                                   ByVal value As Object)
#End Region
#Region "Count from 1 to 32767"
    Private Sub Button1_Click(ByVal sender As System.Object, _
                      ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
        SessionHandler.StartUpLoadData.TimerIsVisible = True
        Process()
        SessionHandler.StartUpLoadData.TimerIsVisible = False
    End Sub
    Private Sub Process()
        'Configure the ProgressBar
        Me.ProgressBar1.Minimum = 0
        Me.ProgressBar1.Maximum = Short.MaxValue
        Me.ProgressBar1.Value = 0
        'Stores the value of the ProgressBar
        Dim value As Double = 0
        'Create a new instance of our ProgressBar Delegate that points
        '  to the ProgressBar's SetValue method.
        Dim updatePbDelegate As New UpdateProgressBarDelegate(AddressOf ProgressBar1.SetValue)
        'Tight Loop:  Loop until the ProgressBar.Value reaches the max
        'Dim Ctr As Long = 0
        'For Ctr = 0 To ProgressBar1.Maximum
        '    If Ctr > ProgressBar1.Maximum Then
        '        Exit For
        '    End If
        '    Ctr += 1
        '    Dispatcher.Invoke(updatePbDelegate, _
        '                      System.Windows.Threading.DispatcherPriority.Background, _
        '                      New Object() {ProgressBar.ValueProperty, CDbl(Ctr)})
        'Next
        Do Until Me.ProgressBar1.Value = Me.ProgressBar1.Maximum
            value += 1
            'Update the Value of the ProgressBar:
            '  1)  Pass the "updatePbDelegate" delegate that points to the ProgressBar1.SetValue method
            '  2)  Set the DispatcherPriority to "Background"
            '  3)  Pass an Object() Array containing the property to update (ProgressBar.ValueProperty) and the new value
            Dispatcher.Invoke(updatePbDelegate, _
                              System.Windows.Threading.DispatcherPriority.Background, _
                              New Object() {ProgressBar.ValueProperty, value})
        Loop
        'Done = True
    End Sub
    Private Sub ProgressBar1_ValueChanged(ByVal sender As System.Object, ByVal e As System.Windows.RoutedPropertyChangedEventArgs(Of System.Double))
        Label1.Content = e.NewValue.ToString
        stepText.Text = "STEP " & e.NewValue.ToString & " completed"
    End Sub
#End Region
#Region "Prime Number calculation"
    Delegate Sub NextPrimeDelegate()
    'Current number to check 
    Private num As Long = 3
    Private continueCalculating As Boolean = False
    'Public Sub New()
    '    InitializeComponent()
    'End Sub 'New
    Private Sub StartOrStop(ByVal sender As Object, ByVal e As EventArgs)
        If continueCalculating Then
            continueCalculating = False
            startStopButton.Content = "Resume"
            SessionHandler.StartUpLoadData.TimerIsVisible = False
        Else
            SessionHandler.StartUpLoadData.TimerIsVisible = True
            continueCalculating = True
            startStopButton.Content = "Stop"
            startStopButton.Dispatcher.BeginInvoke(DispatcherPriority.Normal, New NextPrimeDelegate(AddressOf CheckNextNumber))
        End If
    End Sub 'StartOrStop
    Public Sub CheckNextNumber()
        'Configure the ProgressBar
        Me.ProgressBar1.Minimum = 0
        Me.ProgressBar1.Maximum = 0
        Me.ProgressBar1.Value = 0
        'Stores the value of the ProgressBar
        Dim value As Double = 0
        'Create a new instance of our ProgressBar Delegate that points
        '  to the ProgressBar's SetValue method.
        Dim updatePbDelegate As New UpdateProgressBarDelegate(AddressOf ProgressBar1.SetValue)
        ' Reset flag.
        NotAPrime = False
        Dim i As Long
        For i = 3 To Math.Sqrt(num)
            If num Mod i = 0 Then
                ' Set not a prime flag to ture.
                NotAPrime = True
                Exit For
            End If
        Next i
        ' If a prime number.
        If Not NotAPrime Then
            bigPrime.Text = num.ToString()
        End If
        num += 2
        If continueCalculating Then
            startStopButton.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.SystemIdle, New NextPrimeDelegate(AddressOf CheckNextNumber))
            Dispatcher.Invoke(updatePbDelegate, _
                              System.Windows.Threading.DispatcherPriority.Background, _
                              New Object() {ProgressBar.ValueProperty, value})
        End If
    End Sub 'CheckNextNumber
    Private NotAPrime As Boolean = False
#End Region
#Region "Long Multi-step UI thread Process"
    Private Sub MultiStepUIProcess(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
        ' This just simulates a long multi-step process
        SessionHandler.StartUpLoadData.TimerIsVisible = True
        'Configure the ProgressBar
        Me.ProgressBar1.Minimum = 0
        Me.ProgressBar1.Maximum = 30 ' has 10 steps
        Me.ProgressBar1.Value = 0
        'Stores the value of the ProgressBar
        Dim value As Double = 0
        'Create a new instance of our ProgressBar Delegate that points
        '  to the ProgressBar's SetValue method.
        Dim updatePbDelegate As New UpdateProgressBarDelegate(AddressOf ProgressBar1.SetValue)
        For value = 1 To Me.ProgressBar1.Maximum
            System.Threading.Thread.Sleep(Math.Round(CDbl(3000 / value), 0))
            Dispatcher.Invoke(updatePbDelegate, _
                              System.Windows.Threading.DispatcherPriority.Background, _
                              New Object() {ProgressBar.ValueProperty, value})
        Next
        SessionHandler.StartUpLoadData.TimerIsVisible = False
    End Sub
#End Region
End Class

I have a class called StartupLoadData which implements INotifyPropertyChanged – here I have two properties:

  • TimerIsVisible – this is used to make the progress bar visible and vice versa.
  • CircularProgressBarFill – this is bound to the Fill property of the ellipses in the CircularProgressBar – if you implement themes and allow your users to choose different themes, then you can dynamically change this value by retrieving the user’s state values to match the user’s selected theme.

The SessionHandler has a property called StartUpLoadData where an instance of the above class is stored when the application starts.


The BooleanToVisibilityConverter is used to convert the Boolean value in TimerIsVisible to a Visibility structure.


The StartUpLoadData, SessionHandler and BooleanToVisibilityConverter are really irrelevant and just accessories for this example.


Hope this example makes another developer’s job a bit easier. When the pressure is on to complete a project in time, no one wants to be wasting time trying to figure out how to implement progress bars in WPF!! The examples in CodeProject and other sites on the internet have saved me a lot of time in the past – my opportunity to contribute now!

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here