Cannot get Progress Bar to Update while running tasks in WPF

CodeLiftsleep

Member
Joined
Mar 11, 2016
Messages
20
Programming Experience
3-5
Not sure what I am doing wrong, but I can't get it to work. I am doing a CPU intensive task where I am creating a large number of players for a football game I am making, and I am using the Task.Factory.StartNew method to run it on. I then create a separate task for the Progress updater but I can't seem to get it to work.

I have a WPF progressbar which I have bound to an INotifyPropertyChanged Property, however it does not update and the UI appears frozen, which I thought tasks were supposed to eliminate...

VB.NET:
    TimeIt(Sub() ReallyGenNewPlayers())

    Private Sub TimeIt(MyAction As Action)
        Dim SW As New Stopwatch
        SW.Start()
        MyAction()
        Console.WriteLine($"Total Time Generating Players: {SW.Elapsed} seconds")
        SW.Stop()
    End Sub


    Private Sub ReallyGenNewPlayers()
        Task.Factory.StartNew(Sub() GenNewPlayersASync()).Wait()
    End Sub


     Private Sub GenNewPlayersASync()
        Dim x As Integer = 0
        'Generate the Players on an Async Thread

        For i As Integer = 1 To NumPlayers
            x = i
            Task.Factory.StartNew(Sub() CollegePlayers.GenDraftPlayers(x, MyDraft, DraftDT, DraftClass, PosCount)).Wait()
            Dim mytask As Task(Of Double) = Task(Of Double).Factory.StartNew(Function() UpdateProgressBar(x, NumPlayers))
            ProgBarValue = mytask.Result
        Next i

    End Sub

    Private Function UpdateProgressBar(ByVal playernum As Integer, ByVal TotalPlayers As Integer) As Double
        Dim myval As Double
        Return myval = (playernum / TotalPlayers) * 100
    End Function

Any help would be appreciated, and if you could let me know the how's and why's I would appreciate it as well so I can learn...
 
Ok, a couple of issues here...

1- Why is your viewmodel calling InitializeComponents? The ViewModel is NOT the form designer. The viewmodel is a separate class. Create a new class and call it MyViewModel.vb or something. This is what should be in it, and NOTHING ELSE:

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public Class MyViewModel
    Implements INotifyPropertyChanged

    Private _PlayerCount As Integer = 0
    Private _TotalPlayers As Integer = 0

    ' This property should calculate itself, no point in writing to it, so make it read only.
    Public ReadOnly Property PercentDone As Integer
        Get
            ' Return the calculated percent every time.
            Return CInt((PlayerCount / TotalPlayers) * 100)
        End Get
    End Property

    ' This property sets the current count as set in the GenNewPlayersASync
    ' Note that having your external class dig around properties in your view model
    ' is not ideal, but it should work.
    Public Property PlayerCount As Integer
        Get
            Return _PlayerCount
        End Get
        Set(value As Integer)
            _PlayerCount = value
            NotifyPropertyChanged("PlayerCount")
            ' Every time you change the count, the percentage changes too, so notify the form.
            NotifyPropertyChanged("PercentDone")
        End Set
    End Property

    Public Property TotalPlayers As Integer
        Get
            Return _TotalPlayers
        End Get
        Set(value As Integer)
            _TotalPlayers = value
            NotifyPropertyChanged("TotalPlayers")
        End Set
    End Property

#Region "INotifyPropertyChanged"
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Sub NotifyPropertyChanged(<CallerMemberName> Optional ByVal propname As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propname))
    End Sub
#End Region

End Class


2- Same goes for the MyVM.Show() call in your GeneratePlayers class. If MyVM is the form, then call it MyForm (MyForm.xaml and MyForm.xaml.vb). In the form .VB file, all you should have is glue logic for the UI, event handlers, etc.. So that call needs to go, or change. In your loop you use its reference to set your properties. That reference should be to an instance of your viewmodel. So declare your view model somewhere appropriate. Private myVM = New MyViewModel().

3- I don't see where in your XAML you connect the form to the view model. Either add the datacontext in the XAML like I did above, or in the code behind set the form's DataContext property to your instance of your view model. Form1.DataContext = myVM.

4- Normally, the code in your GeneratePlayers class should not even be aware that a viewmodel exists, and vice versa. The form (XAML) connects to the view model, and the view model connects to the model (model is a generic term for "the program"). The model should NOT depend on the view model. So there are still some architectural issues that should be fixed.

One way to separate your GeneratePlayers class and view model is through events. Instead of writing directly to your view model from the GeneratePlayers class, have your async method raise an event every time a player is generated.
 
Last edited:
Ok, a couple of issues here...

1- Why is your viewmodel calling InitializeComponents? The ViewModel is NOT the form designer. The viewmodel is a separate class. Create a new class and call it MyViewModel.vb or something. This is what should be in it, and NOTHING ELSE:

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Public Class MyViewModel
    Implements INotifyPropertyChanged

    Private _PlayerCount As Integer = 0
    Private _TotalPlayers As Integer = 0

    ' This property should calculate itself, no point in writing to it, so make it read only.
    Public ReadOnly Property PercentDone As Integer
        Get
            ' Return the calculated percent every time.
            Return CInt((PlayerCount / TotalPlayers) * 100)
        End Get
    End Property

    ' This property sets the current count as set in the GenNewPlayersASync
    ' Note that having your external class dig around properties in your view model
    ' is not ideal, but it should work.
    Public Property PlayerCount As Integer
        Get
            Return _PlayerCount
        End Get
        Set(value As Integer)
            _PlayerCount = value
            NotifyPropertyChanged("PlayerCount")
            ' Every time you change the count, the percentage changes too, so notify the form.
            NotifyPropertyChanged("PercentDone")
        End Set
    End Property

    Public Property TotalPlayers As Integer
        Get
            Return _TotalPlayers
        End Get
        Set(value As Integer)
            _TotalPlayers = value
            NotifyPropertyChanged("TotalPlayers")
        End Set
    End Property

#Region "INotifyPropertyChanged"
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Sub NotifyPropertyChanged(<CallerMemberName> Optional ByVal propname As String = Nothing)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propname))
    End Sub
#End Region

End Class


2- Same goes for the MyVM.Show() call in your GeneratePlayers class. If MyVM is the form, then call it MyForm (MyForm.xaml and MyForm.xaml.vb). In the form .VB file, all you should have is glue logic for the UI, event handlers, etc.. So that call needs to go, or change. In your loop you use its reference to set your properties. That reference should be to an instance of your viewmodel. So declare your view model somewhere appropriate. Private myVM = New MyViewModel().

3- I don't see where in your XAML you connect the form to the view model. Either add the datacontext in the XAML like I did above, or in the code behind set the form's DataContext property to your instance of your view model. Form1.DataContext = myVM.

4- Normally, the code in your GeneratePlayers class should not even be aware that a viewmodel exists, and vice versa. The form (XAML) connects to the view model, and the view model connects to the model (model is a generic term for "the program"). The model should NOT depend on the view model. So there are still some architectural issues that should be fixed.

One way to separate your GeneratePlayers class and view model is through events. Instead of writing directly to your view model from the GeneratePlayers class, have your async method raise an event every time a player is generated.

Created a new ViewModel and have it exactly as you do, bound using <Window.DataContext>. Previously had it set in the code behind which I didn't show in my example, sorry for the confusion. Still not working. I did get the Progress Bar Window to be responsive by Calling the Button Click Event Async and then changing

VB.NET:
TimeIt(GenSubAsync()) to Await Task.Run(Sub() TimeIt(GenSubAsync())
.

However, even though I can now drag the Progress Bar Window around on the screen after it pops up when I enable it as it runs, the values still won't update even though they are bound to the Properties.
Property values are updating properly, just not showing in their respective places.

I'm at a loss right now...this is supposed to something really simple and I've literally been working at this a day and a half and its still not working...beyond frustrating.
 
I'm not really into WPF either, but I noticed in your post 13 there was spelling mistake in your binding: {Binding PerecentDone}
 
It's correct in the actual code, its just a typo in here...wish that was the problem tho...lol

Why aren't you copying and pasting your actual code?
 
Why aren't you copying and pasting your actual code?

I did, but I must have changed it in the program after pasting it once it threw an error and then forgot to change it here.

Anyways, I have somewhat figured out what is going wrong.

I have a third moving part involved.

I have a GeneratePlayers Page, the ViewModel and then the ProgressBarDialog Window

Basically here is what I want to happen. When the user clicks the button to Generate Players from the GeneratePlayers page, that page gets disabled and it Opens up a ProgressBarDialong Window on top of it saying "Generating Players...." and then the progress bar updates while its generating players until its finished, at which point it closes itself and control returns back to the GeneratePlayers Page. I got the opening and closing, etc working but not the updating of the progress bar.

Here is what I need to happen:

1) GeneratePlayers page opens up a new instance of the ProgressBarDialog Window once the button is clicked
2) ProgressBarDialog instance stays open and updates itself from the ViewModel Properties that its bound to.
3) ViewModel however is getting its values from the GeneratePlayers Page, which I believe is making it so two separate instances of the ViewModel are being used...Ie, the GeneratePlayers instance is updating the values, but the ProgressBarDialong instance is reading its own values which are not being updated and are always 0. Do I need to make the ViewModel NotInheritable and its Properties/Methods as Shared? Make it a Module? VB is such a pain in the ass compared to C# where I could just make it static....

Whats the easiest way to make these 3 parts work together the way they need to?

UPDATE: OK, I added some code to help me see what is going on...

I have it printing to the console the System.Threading.Thread.CurrentThread.ManagedThreadID
So the original GeneratePlayers Thread is 8 for example, then I call:

--->Prog.Show() --->On Thread 8
--->Window.GetWindow(Prog) ----> On Thread 8

VB.NET:
Await Task.Run(Sub()


                           Console.WriteLine($"This is the current thread 2: { System.Threading.Thread.CurrentThread.ManagedThreadId}")
                           TimeIt(Sub() GenNewPlayersASync(MyVM, Prog))
                       End Sub)

Which along with declaring the ButtonClick as Async frees up the Progress Bar Window so I can move it around, etc when it pops up(its not stuck like it was before).
This and everything past this becomes Thread 10...

and checking Prog.Dispatcher.CheckAccess() inside Thread 10 is returning False(which makes sense since its on Thread 8)...which means I need to run Prog.Dispatcher.BeginInvoke to gain access to it and then update the UI from within it, correct?


I've also seen where they say to open the window inside the thread you need it to be in but then it throws an error saying the method needs to declared as an <STAThread because many UI components require this...so I'm not sure which way to go about this...
However, I do that and it doesn't do anything...getting closer, but seems like I'm still far away...
 
Last edited:
FINALLY!!!! Got it working---I called Prog.Dispatcher.BeginInvoke(Sub() LoopPercent(byval x as integer, byval MyVM as ProgressBarDialogViewmodel) inside the player generation loop...don't have it bound via UI, but its working by directly updating the progress.Value inside the Loop...now I need to figure out how to get the binding to work properly...

VB.NET:
    Private Sub GenNewPlayersASync(ByVal MyVM As ProgressBarDialogViewModel, ByVal Prog As ProgressBarDialog)


        Dim x As Integer = 0
        'Generate the Players on an Async Thread
        Console.WriteLine($"This is the current thread 4: {System.Threading.Thread.CurrentThread.ManagedThreadId}")
        Console.WriteLine($"Progress Bar Has Access? {Prog.Dispatcher.CheckAccess()}")


        For x = 1 To MyVM.TotalPlayers
            CollegePlayers.GenDraftPlayers(x, MyDraft, DraftDT, DraftClass, PosCount)
            Dim i As Integer = x
            MyVM.PlayerCount = i
[B]            If MyVM.PlayerCount > 1 And Not Prog.Dispatcher.CheckAccess() Then[/B]
[B]                Prog.Dispatcher.BeginInvoke(Sub() Prog.LoopPercent(x, MyVM))[/B]
[B]            End If[/B]
        Next x
    End Sub

Inside the ProgressBar Model itself:

VB.NET:
[B]    Public Sub LoopPercent(ByVal x As Integer, ByVal MyVM As ProgressBarDialogViewModel)[/B]
[B]        progress.Value = CInt(((x / MyVM.TotalPlayers) * 100))[/B]
[B]    End Sub[/B]

Damn...2 days but I got it figured out!
 
Last edited:
Back
Top