Visual Basic.NET: how to create a thread to update the UI
Asked Answered
C

3

4

The usual VB way to handle a computationally heavy task is to put it in a background worker thread, while the main thread keeps handling the UI.

Say for whatever reason I needed to do this the other way around: the main thread doing the grunt work and the background one updating the UI.

Here's what I have so far. The only problem is, while the UI window (Form1) does get redrawn, you can't interact with it, not even move or resize it (the mouse cursor turns to hourglass and won't click).

Public Class ProgressDisplay

Private trd As Thread

    Public Sub New()
        trd = New Thread(AddressOf threadtask)
        trd.Start()
    End Sub

    Private Sub threadtask()
        Dim f1 As Form1
        f1 = New Form1
        f1.Show()
        Do
            f1.Update()
            Thread.Sleep(100)
        Loop
    End Sub

End Class

Edit: Ideally I need to present an interface like this to the client

Public Class ProgressDisplay
    Public Sub New()
    Public Sub Update(byval progress as int)
End Class

The client will call it like this (actually in unmanaged c++ over COM but you get the picture):

Dim prog = new ProgressDisplay()
DoLotsOfWork(addressof prog.update) ' DoLotsOfWork method takes a callback argument to keep client informed of progress
Communion answered 22/6, 2012 at 11:51 Comment(6)
Trouble is the long work isn't being done in my code - it's happening in the client for this class. So, I can't control where it happens - it's in the main thread. What the client needs to do is call a SetupProgressIndicator method at the start, and an IncrementProgressIndicator method from time to time. Can that be made to work?Communion
Please describe the situation in more detail. It is difficult to understand what you mean.Metcalfe
You seem to be using the word "client" to refer both to the code using the ProgressDisplay, and the ProgressDisplay object itself. I'm getting confused too.Bentlee
The client is the code that uses ProgressDisplay. It is also the client of the method DoLotsOfWork(progress_callback). Where do I use client to refer to ProgressDisplay itself?Communion
You said "DoLotsOfWork method takes a callback argument to keep client informed of progress". Is the call-back method not a delegate to the ProgressDisplay.Update method? If so, to sounds like your saying ProgressDisplay is the client.Bentlee
I mean that the code that uses ProgressDisplay is the client. It takes the callback and forwards it on to ProgressDisplay.Update. Sorry for the confusion :)Communion
M
5

To clearly restate the problem - you need to provide a visual component to a client who will use it within their own program. The client's program, outside of your control, ties up its main(ie:UI) thread and you are required to have your visual component continue working while the client's program is frozen.

Let's be clear here - the fact that the client's program is freezing is their problem and it is the direct result of defects in their application code. What follows below is a hack, and an awful one that nobody should ever be using. So, while you CAN do the following, it should almost never be considered a recommended solution for anything. It flies in the face of all best-practice.

With that said, you need to create a second application context and a new message loop which can continue running beside the main UI thread. A class something like this would work :

Imports System.Threading

Public Class SecondUIClass

    Private appCtx As ApplicationContext
    Private formStep As Form
    Private trd As Thread
    Private pgBar As ProgressBar
    Delegate Sub dlgStepIt()

    Public Sub New()
        trd = New Thread(AddressOf NewUIThread)
        trd.SetApartmentState(ApartmentState.STA)
        trd.IsBackground = True
        trd.Start()
    End Sub

    Private Sub NewUIThread()
        formStep = New Form()
        pgBar = New ProgressBar()
        formStep.Controls.Add(pgBar)
        appCtx = New ApplicationContext(formStep)
        Application.Run(appCtx)
    End Sub

    Public Sub StepTheBar()
        formStep.Invoke(New dlgStepIt(AddressOf tStepIt))
    End Sub

    Private Sub tStepIt()
        pgBar.PerformStep()
    End Sub

End Class

Essentially what you are doing with the above class is creating a new application context within a new STA thread (giving that thread a message loop). That context contains a main form (giving the thread ownership of it and responsibility for its message processing) which can continue to operate aside from the main UI thread. This is much like having a program within a program - two UI threads, each with their own mutually exclusive set of controls.

Calls which interact with any of the controls owned by the new UI thread (or its form) must be marshalled with Control.Invoke from the primary UI thread (or others) to ensure that your new UI thread is the one doing the interaction. You could also use BeginInvoke here.

This class has NO cleanup code, no checks for safety, etc (be warned) and I'm not even sure it would finish up gracefully - I leave that task to you. This just illustrates a way to get started. In the main form you would do something like :

Public Class Form1

    Private pgClass As New SecondUIClass

    Private Sub Button1_Click(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles Button1.Click
        Dim i As Integer
        For i = 1 To 10
            System.Threading.Thread.Sleep(1000)
            pgClass.StepTheBar()
        Next
    End Sub
End Class

Running the application above would create Form1 as well as a second form created by pgClass. Clicking Button1 on Form1 would lock up Form1 while it went through its loop but the second form would still remain alive and responsive, updating its progress bar each time Form1 called .StepTheBar().

The best solution in this case, really, is to have the 'client' learn how to program properly and avoid being stuck in this conundrum in the first place. In the case where that is completely impossible and you MUST create for them a component whch will remain alive despite their poor code then the above approach is probably your only recourse.

Metcalfe answered 22/6, 2012 at 12:43 Comment(0)
B
3

The UI can only be updated via the thread that created it. Any thread can make a request to invoke a method on the UI thread so that it can update the UI, by using the Control.Invoke method, but that will just sit and wait until the UI thread is no longer busy. If the UI thread is busy, the UI cannot be updated by anything or anyone. The UI only gets updated when the main message loop (AKA Application.Run) processes the window messages in the queue and acts upon them. If the UI thread is busy, stuck in a loop or waiting for a response from a server, it can't process those window messages (unless you call DoEvents, which I definitely do not recommend, if at all possible). So, yes, while the UI is busy, it will be locked up. That's the whole reason why everyone suggests doing any hefty business logic in a separate thread. If that wasn't an issue, why would anyone bother making worker threads?

Bentlee answered 22/6, 2012 at 12:0 Comment(0)
M
2

The UI can't be updated from any other thread than the main event dispatching thread. So you are trying to do something that won't work.

Mure answered 22/6, 2012 at 11:54 Comment(1)
Ok. Is there a way to fire off another process that can handle this?Communion

© 2022 - 2024 — McMap. All rights reserved.