0

I am currently working on a VB.NET form that automatically create Word documents according to an Excel file and a few extra data asked by the form (Project Name, Customer Name, Use SQL, ...).

This procedure works fine and takes approximatelly 1 or 2 minutes to complete. The issue is that all my script is in ButtonGenerate.Click Handler. So when the Generate button is pressed the form window is bricked and it's impossible to Cancel...

It shouldn't be in a Click handler. Opening a new thread for that long task seems better. But Multithreading isn't very familiar to me. I tryed launching the script with

ThreadPool.QueueUserWorkItem(...

but my Generate Sub sets labels and update a Progress Bar in the main form, so I doesn't work unless I use

Me.Invoke(New MethodInvoker(Sub()
    label.Text = "..."
    ProgressBar.Value = 10
    ' ...
End Sub)

each time I need to update something on the form and I can't even retrieve any new push of a button with that (A cancel button would be nice).

This is basically my code :

Public Class TestFichesAutomation

Private Sub BtnGenerate_Click(sender As Object, e As EventArgs) Handles BtnGenerate.Click
    System.Threading.ThreadPool.QueueUserWorkItem(Sub() Generate())
End Sub

Public Sub Generate()
    ' Check user options, retrieve Excel Data, SQL, Fill in custom classes, create Word docs (~ 1 minute)
End Sub


So How would you handle that script ? Is Threading even a good solution ?

  • this might help: https://stackoverflow.com/questions/27565851/progress-bar-and-background-worker – Jan Jul 19 '19 at 11:24
  • Possible duplicate of [Progress Bar and Background Worker](https://stackoverflow.com/questions/27565851/progress-bar-and-background-worker) – Peter Duniho Jul 19 '19 at 12:46

2 Answers2

2

Thanks a lot for your help ^^ and for the useful doc.

My app now open a new thread and uses 2 custom classes to act like buffers :

Private Async Sub Btn_Click(sender As Object, e As EventArgs) Handles Btn.Click
     myProgress = New Progress
    ' a custom class just for the UI with the current task, current SQL connection status and progress value in %

    _Options.ProjectName = TextBoxProjectName.Text
    _Options.CustomerName = TextBoxCustomerName.Text
    ...
    ' Fill in a custom "_Options" private class to act as a buffer between the 2 thread (the user choices)       

    Loading = New Loading()
    Me.Visible = False
    Loading.Show() ' Show the Loading window (a ProgressBar and a label : inputLine)

    Task.Run(Function() Generate(Progress, _Options))
    Me.Visible = True
End Sub


Public Async Function Generate(ByVal myProgress As Progress, ByVal Options As Options) As Task(Of Boolean)
    ' DO THE LONG JOB and sometimes update the UI :
    myProgress.LoadingValue = 50 ' %
    myProgress.CurrentTask= "SQL query : " & ...
    Me.Invoke(New MethodInvoker(Sub() UpdateLoading()))
    ' Check if the task has been cancelled ("Cancelled" is changed by a passvalue from the Loading window):
    If myProgress.Cancelled = True Then ...
    ' Continue ...
End Function


Public Shared Sub UpdateLoading()
    MyForm.Loading.ProgressBar.Value = myProgress.LoadingValue
    MyForm.Loading.inputLine.Text = myProgress.CurrentTask
    ' ...
End Sub
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
0

You should look into using the Async/Await structure

if the work you need to do is CPU bound, i like using Task.Run() doc here

By making your event handler Async and having it Await the work, you prevent the UI from locking up and avoid the use of Invoke in most cases.

ex:

Private Async Sub Btn_Click(sender As Object, e As EventArgs) Handles Btn.Click
    Dim Result As Object = Await Task.Run(Function() SomeFunction())
    'after the task returned by Task.Run is completed, the sub will continue, thus allowing you to update the UI etc..

End Sub

For progress reporting with Async/Await you might be interested in this

  • Although I would generally favor `async` and `await`, since the question wants to update the UI, you can't just hand-wave away the `Invoke` as long as the work is being done on another thread as with `Task.Run`---and I'd guess this is sufficiently CPU-bound to be worth doing so. – Craig Jul 19 '19 at 17:03
  • i'd say there's ways around it, but you might be right that it's easier to just use `invoke`. However since you need `invoke` in either case (`Async/Await`or more directly using the threadpool), i'd still go for `Async/Await`as imo it leads to cleaner code. – AConfusedSimpleton Jul 20 '19 at 08:19
  • 1
    I definitely prefer `Async/Await`, I didn't mean to suggest otherwise. – Craig Jul 22 '19 at 13:02
  • Then i misunderstood you, re-reading your comment i can indeed see that's not what you said. – AConfusedSimpleton Jul 22 '19 at 14:46
  • No worries, I might have been a little too terse and so a little got lost. I was trying to say that while there are many benefits to using `Async` and `Await`, they don't get you out of needing to use `Invoke` to update the UI from code running on a non-UI thread. Really, I'd say they're somewhat orthogonal. You very well might `Await` something spun up using `Task.Run()`, and also need to use `Invoke` from the spun up code in some form or fashion to get a UI update. – Craig Jul 22 '19 at 14:54
  • 1
    Agreed, separate from the `Async/Await`matter : I think you can prevent the use of `invoke` if you use an atomic operation to increment a progress variable like `PercentComplete`, assuming you calculate the total work needed in advance. The UI Thread could then check it periodically and update the progressbar. – AConfusedSimpleton Jul 23 '19 at 08:13