Updating WinForms interface (NOT WPF) from another thread

EDIT: The sample code is far from the original goal, but there is a legitimate answer to the question I actually posed, so I edited the question and title.

I am writing a multi-threaded PowerShell script with WinForms GUI and would like to update the GUI from one of the other threads in the script. I had no luck doing this at all. In fact, I can't even get it to BeginInvoke

work.

Here's a very stripped down example. This is sample code, not production, so it's not pretty, but it replicates the problem:

$script:SynchronizedUIHash = [Hashtable]::Synchronized(@{});

[reflection.assembly]::LoadWithPartialName( "System.Windows.Forms")
$form= New-Object Windows.Forms.Form
$label = New-Object Windows.Forms.Label
$label.Text = 'original'
$script:SynchronizedUIHash.label = $label

$form.Controls.add($label)
$form.show()
[System.Windows.Forms.Application]::DoEvents()

Read-Host

$label.BeginInvoke(
    [Action[string]] {
        param($Message)
        [Console]::Beep(500,300)
        $l = $SynchronizedUIHash.label
        $l.Text = $Message
        [System.Windows.Forms.Application]::DoEvents()
    },
    'changed'
)

Read-Host

$form.close()

      

I don't even hear the console beep, so the code BeginInvoke

doesn't execute at all.

If I close [System.Windows.Forms.Application]::DoEvents()

after Read-Host

then I see the change, so it seems like it’s a timing issue, but I don’t want to go into the watch face or anything stupid - I want to shoot and forget BeginInvoke

, and the results will be visible accordingly. I see other answers such as PowerShell: Form action event not executed , but it should be in check, sleep, check loop.

There must be an easier way that I am missing ... what is it?

What's the correct solution here?

EDIT: Keith's comment made me realize that I missed a key point going from production to the collapsible sample code - starting a job is indeed multiprocessing, but the sample code is not, so the sample does not reflect real life and I will have to rethink the whole thing. All that said - the sample code does not use jobs and it does not work as I expect, so I would like to understand why, even if it doesn't help me in this particular case.

+3


source to share


2 answers


You cannot have console and Windows running in the same thread. I suggest you create another thread to host the window, for example (this is a C # console application, but needs to be easily translated):

class Program
{
    static void Main(string[] args)
    {
        Form form = new Form();
        Label label = new Label();
        label.Text = "original";
        form.Controls.Add(label);

        // run this form on its own thread, so it can work properly
        Thread t = new Thread((state) => Application.Run((Form)state));
        t.Start(form);

        Console.ReadLine();

        // note you *could* do label.Text = "other" here, but BeginInvoke is always recommended
        label.BeginInvoke((Action)delegate() { label.Text = "other"; });

        Console.ReadLine();
        Application.Exit();
    }
}

      

EDIT : It took me a while to get the Powershell version ready, but here it is. The trick is that you simply cannot create a standard .NET thread from a Powershell environment. Period. As soon as you try to do this, you will come across Powershell. The main error that causes the crash is this:

System.Management.Automation.PSInvalidOperationException: No Runspace is available to run scripts on this thread. You can provide one in the DefaultRunspace property of System.Management.Automation.Runspaces.Runspace type



And you cannot try to set this DefaultRunspace variable from another thread as it is marked as threadstatic (one copy per thread)! Now if you google for multi-threaded Powershell you will find many articles about Powershell Jobs. But they allow multitasking, but this is not very multithreading, it is more heavy.

So ... it turns out that the solution is actually using Powershell features, i.e.: Powershell space. Here he is:

[reflection.assembly]::LoadWithPartialName( "System.Windows.Forms")
$form = New-Object System.Windows.Forms.Form
$label = New-Object System.Windows.Forms.Label
$label.Text = 'original'
$form.Controls.add($label)

$ps = [powershell]::create()
$ps.AddScript(
     {
     [System.Windows.Forms.Application]::Run($form)
     })
$ps.Runspace.SessionStateProxy.SetVariable("form", $form)
$ps.BeginInvoke()

Read-Host

$label.BeginInvoke(
    [Action[string]] {
        param($Message)
        $label.Text = $Message
    },
    'changed'
)

Read-Host

[System.Windows.Forms.Application]::Exit()
$ps.Dispose()

      

+2


source


Invoke () - works through threads. Your PowerShell script will get its own process. At this level, you are looking at inter-process communication solutions such as remote control or message queuing rather than inter-thread communications such as Invoke ().



From Powershell, the easiest one might be your own little "message queue" where powershell writes something to a text file that the winforms application looks at and knows what to do when data comes in.

0


source







All Articles