Reputation: 3732
To track progress on a long-running async task in my VB.NET app, I create a small form that contains a ProgressBar control. The task updates the ProgressBar via Invoke, and I check to make sure the control is not Nothing and also that it is not IsDisposed. The idea is that the user can close the ProgressBar form if they don't want to watch it slowly tick towards completion.
However in testing, I closed the form and then got an error "System.ObjectDisposedException: 'Cannot access a disposed object." My guess here is that the form closed while the update was occurring. That is, after I checked if it was OK but before it completed.
Private Sub Btn_StoreForceStale_Click(sender As Button, e As EventArgs) Handles Btn_StoreForceStale.Click
Dim pb As ProgressBar = GetProgressPopUp("Force Refreshing All Stale Items in Inventory")
EnableButtons(False)
Dim x As Task = Task.Run(Sub() RefreshPrices(True,, pb)).ContinueWith(Sub() EnableButtons(True))
End Sub
Public Sub RefreshPrices(Optional force As Boolean = False, Optional PriceAge As Integer = -1, Optional progBar As TextProgressBar = Nothing)
'Actual price refreshing Code
If Not (progBar Is Nothing OrElse progBar.IsDisposed) Then
progBar.Invoke(Sub() progBar.Increment(1)) '<-- Error happens here
End If
End Sub
After the crash and I'm in debug, I can see that progBar.IsDisposed is true and so the increment should not happen. That's why I believe the form closing happened during the increment itself.
Is there a way to avoid this? A way in the Form.Closing event to check if a control is being invoked and delay closing until it's not? Or is something else going on?
Thanks!
Upvotes: 1
Views: 853
Reputation: 70671
Is there a way to avoid this?
Yes. There are many ways. One option is to use Progress(Of T)
to handle updates. Create the instance of the class in the UI thread before you start the task. It will capture the current synchronization context and use that instead of trying to invoke through a specific UI object.
Another way is to use a variable to indicate whether the progress should be reported or not. You can handle the FormClosed
event to set the variable. If you do this though, you need to use some kind of synchronization (e.g. SyncLock
) to ensure that the progress update occurs only when the variable is False
, and that the UI thread can't set it to True
until after the call to BeginInvoke()
occurs. Note that you must use BeginInvoke()
in this approach because you need to hold the lock while you're making the call, and if you used Invoke()
in that situation, the call would deadlock.
As you might guess, I prefer the Progress(Of T)
option. :) It seems a lot less messy to me.
Finally, there's not enough context in your question to know if it would be possible to use Async
and Await
. But if you can recompose your long-running task as a loop that uses Task.Run()
for each iteration, awaiting that and then updating the progress bar, this would IMHO be the ideal approach. Note that each iteration of the loop need not correspond to a single unit of work; you could (and should, if a single unit of work is small) do the work in batches, so that you don't make the transition between worker thread and UI thread too frequently.
Upvotes: 2