0

I have a WebClient in the BackgroundWorker, but for some reason it doesn't start downloading when I'm creating an object before starting it. It works fine when on main thread.


Like this it doesn't work:

Dim AddRPB As New ProgressBar
Dim client As New WebClient
AddHandler client.DownloadProgressChanged, AddressOf DownloadingProgress
AddHandler client.DownloadDataCompleted, AddressOf DownloadComplete
client.DownloadDataAsync(New Uri(WebLink), Data)

Like this it works:

Dim client As New WebClient
AddHandler client.DownloadProgressChanged, AddressOf DownloadingProgress
AddHandler client.DownloadDataCompleted, AddressOf DownloadComplete
client.DownloadDataAsync(New Uri(WebLink), Data)
Dim AddRPB As New ProgressBar

Dim AddRPB As New ProgressBar

That single line breaks it somehow, I don't understand why.

CruleD
  • 1,153
  • 2
  • 7
  • 15
  • There's no sense to using a `BackgroundWorker` if you're calling `DownloadDataAsync`. The point of a `BackgroundWorker` is to do work on a secondary thread. The point of `DownloadDataAsync` is to download data on a secondary thread. What's the point of the `BackgroundWorker` if you're going to download the data on a secondary thread anyway? – jmcilhinney Nov 15 '18 at 22:48
  • 1
    It's hard to say based on the code that you have posted but the issue may well have to do with the fact that you're creating a `ProgressBar` on a secondary thread when you're using the `BackgroundWorker`. That just doesn't make sense because a control is part of the UI so inherently foreground. I suggest that you just get rid of the `BackgroundWorker`. If you have some other need for it, still call `DownloadDataAsync` on the UI thread. – jmcilhinney Nov 15 '18 at 22:51
  • Download is only part of it, keep on topic please, why doesn't work when its created before, but works when it's created after, that is the question. – CruleD Nov 15 '18 at 23:14
  • He is on-topic. jmcilhinney's last comment describes exactly why you might be experiencing this behaviour: You are creating a `ProgressBar` (which is a UI element) in a background thread. Doing so is bad, and that line throwing an exception could be a possible reason as to why it stops working. Always make sure to leave _**all**_ UI-related work on the UI thread _**only**_. – Visual Vincent Nov 16 '18 at 06:29
  • @VisualVincent There is no exception or error, Sub completes normaly other than the fact that Download doesn't start when object is created before it, when it is created after it everything works as expected, how do you explain that? – CruleD Nov 16 '18 at 12:29

1 Answers1

1

This might not be completely accurate, but here's what I came up with through some testing and with help from the Reference Source:

Without/before the instantiation of the ProgressBar

The WebClient works with SynchronizationContexts in order to post data back to the UI thread and invoke its event handlers (as does the BackgroundWorker). When you call one of its Async methods the WebClient immediately creates an asynchronous operation that is bound to the SynchronizationContext of the calling thread. If a context doesn't exist, a new one is created and bound to that thread.

If this is done in the RunWorkerAsync event handler without (or before) creating the ProgressBar, a new synchronization context will be created for the BackgroundWorker's thread.

So far so good. Everything still works but the event handlers will be executed in a background thread rather than the UI thread.

Creating the ProgressBar before starting the download

With the ProgressBar instantiation code in place before the download is started you're now creating a control in a non-UI thread, which will result in a new SynchronizationContext being created and bound to that background thread along with the control itself. This SynchronizationContext is a little different in that it is a WindowsFormsSynchronizationContext, which uses the Control.Invoke() and Control.BeginInvoke() methods to communicate with what they consider to be the UI thread. Internally these methods post a message to the UI's message pump, telling it to execute the specified method on the UI thread.

This appears to be where things go wrong. By creating a control in a non-UI thread and thus creating a WindowsFormsSynchronizationContext in that thread, the WebClient will now use that context when invoking the event handlers. The WebClient will call WindowsFormsSynchronizationContext.Post(), which in turn calls Control.BeginInvoke() to execute that call on the synchronization context's thread. The only problem is: That thread has no message loop that will handle the BeginInvoke message.

  • No message loop = The BeginInvoke message won't be handled

  • The message won't be handled = Nothing calls the specified method

  • The method isn't called = The WebClient's DownloadProgressChanged or DownloadDataCompleted events will never be raised.

In the end all this just once again boils down to the golden rule of WinForms:

Leave all UI related work on the UI thread!


EDIT:

As discussed in the comments/chat, if all you are doing is passing the progress bar to the WebClient's asynchronous methods, you can solve it like this and let Control.Invoke() create it on the UI thread and then return it for you:

Dim AddRPB As ProgressBar = Me.Invoke(Function() New ProgressBar)

AddHandler client.DownloadProgressChanged, AddressOf DownloadingProgress
AddHandler client.DownloadDataCompleted, AddressOf DownloadComplete

client.DownloadDataAsync(New Uri(WebLink), AddRPB)
Community
  • 1
  • 1
Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • I found out later than even if I create the progressbar after DownloadDataAsync, progressbar can be found when progresschanged triggers (though I"m not sure if it would if it's some tiny file). I needed the async so I could pass some data so i can find the progressbar in progress changed/download complete events. But since then I rewrote the code and is created before as intended and more stuff is on main thread. – CruleD Nov 16 '18 at 19:07
  • @CruleD : Well you simply should never create controls in a background thread :). I'm currently editing my answer with a more detailed explanation, by the way. So stay tuned for an update. – Visual Vincent Nov 16 '18 at 19:09
  • Why not, This is the first time I encountered a strange issue like that, and surely there are times when you need to create a control from some task that is running in the background. Guess it's just another one of those strange microsoft kinks. – CruleD Nov 16 '18 at 19:09
  • @CruleD : It's not a "kink" by any means. In all operating systems multithreaded code can easily be prone to _Race conditions_. Each UI thread in Windows is driven by a message pump which isn't meant to be tampered with across different threads (how should it know which thread should access which control first, and what happens if they do it in the wrong order?). It's always up to the programmer to write thread-safe code as there will never be an all-round solution. – Visual Vincent Nov 16 '18 at 19:14
  • @CruleD : _"Why not"_ - You should never need to create a control in a background thread. If you feel that you need to do that then you're doing something wrong and need to rethink your model. _Data_ and _heavy processing_ should be taken care of in a background thread, then passed to the UI thread which adds/updates the appropriate controls for it. – Visual Vincent Nov 16 '18 at 19:16
  • @CruleD : _"surely there are times when you need to create a control from some task that is running in the background"_ - In my 6+ years of doing multithreaded programming, I have actually _**never**_ felt the need to create a control in a background thread. :) – Visual Vincent Nov 16 '18 at 19:21
  • As far as I know if I call any method from BGW it's also executed on BG thread. How would I execute "CreatePB()" method on main thread in example from OP? – CruleD Nov 16 '18 at 19:21
  • @CruleD : By scheduling it for execution on the UI thread using [`Control.Invoke()`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.invoke) or [`Control.BeginInvoke()`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.control.begininvoke). Please see the following answer of mine, I've covered this topic a bit more thorougly there: [How can I run code in a background thread and still access the UI?](https://stackoverflow.com/a/45571728/3740093) – Visual Vincent Nov 16 '18 at 19:24
  • But Invoke is only needed when you want to add it somewhere (crossthread error otherwise) like Me.Controls.Add(AddRPB). So you are saying I should do Me.invoke( from dim AddRPB to control.add)? – CruleD Nov 16 '18 at 19:30
  • @CruleD : No, invoking is required _**EVERY TIME**_ you want to access the UI _**from a background thread**_ or even deal with UI elements in any fashion (_such as when creating controls!_). It is _**NOT**_ required _**only**_ when adding controls. -- However this doesn't stop you from calling `Invoke` _multiple times_, so you _don't_ need to wrap everything in an invoke (and you shouldn't). – Visual Vincent Nov 16 '18 at 19:33
  • I was talking about this specific case, there was invoke for control.add as it is required, should have I put the whole thing in the invoke (from dim addRPB to control.add)? – CruleD Nov 16 '18 at 19:37
  • @CruleD : This specific case or any case. You still need to invoke every time you're doing anything that has got to do with the UI (even when only creating _controls_ without adding them). But could you please explain to me _why_ you want/need to create this progress bar in a background thread? I could help you find a better way around it. – Visual Vincent Nov 16 '18 at 19:39
  • (By the way I have updated my answer above with a more detailed explanation) – Visual Vincent Nov 16 '18 at 19:39
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/183797/discussion-between-visual-vincent-and-cruled). – Visual Vincent Nov 16 '18 at 19:40