EDIT: The sample code is far off from the original goal, but there’s a legitimate answer to the question that I actually posed, so I’ve edited the question and title.
I am writing a multithread PowerShell script with a WinForms GUI and would like to update the GUI from one of the other threads in the script. I’m having no luck doing this at all. In fact, I can’t even get BeginInvoke to work.
Here’s a very stripped down example. This is sample code, not production, so it’s not pretty, but it’s replicating the issue:
$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 it’s as if the BeginInvoke code isn’t being executed at all.
If I throw in a [System.Windows.Forms.Application]::DoEvents() after the Read-Host then I see the change so it seems to be a timing issue, but I don’t want to go into a spin loop or something silly like that – I want to fire and forget the BeginInvoke and have the results visible as appropriate. I see other answers like the one to PowerShell: Job Event Action with Form not executed but that needs to be in a check, sleep, check loop.
There’s got to be a simpler way that I’m missing… what is it?
What is the right solution here?
EDIT: Keith’s comment made me realize I missed a key point going from production to the stripped sample code – launching a job is indeed multi-process, but the sample code isn’t, so the sample doesn’t reflect real life, and I’ll have to rethink the whole thing. That all said – the sample code doesn’t use jobs, and it doesn’t work as I expect either, so I’d like to understand why, even if that doesn’t help me in this specific case.
You can’t really have a Console and a Windows run on the same thread. I suggest you create another thread to host the window, like this (It’s a C# console app, but it should be easy to translate):
EDIT: It took me a while to cook a Powershell version, but here it is. The trick is: you just can’t create a standard .NET thread from the Powershell environement. Period. As soon as you try to do it, you will crash Powershell. The underlying error that causes the crash is this:
And, you can’t really try to set this DefaultRunspace variable from another thread because it’s marked threadstatic (one copy per thread)! Now, if you google for multi-threading Powershell, you will find many articles about Powershell Jobs. But these allow multi-tasking, but it’s not really multi-threading, it’s more heavyweight.
So… turns out the solution is really to use Powershell features, that is: Powershell runspace. Here it is: