While you can create controls on thread B, you cannot add them to a control that was created in thread A from thread B.
If you attempt that, you'll get the following exception:
Controls created on one thread cannot be parented to a control on a different thread.
Parenting to means calling the .Add()
or .AddRange()
method on a control (form) to add other controls as child controls.
In other words: In order to add controls to your $Main
form, which is created and later displayed in the original thread (PowerShell runspace), the $Main.Controls.Add()
call must occur in that same thread.
Similarly, you should always attach event delegates (event-handler script blocks) in that same thread too.
While your own answer attempts to ensure adding the buttons to the form in the original runspace, it doesn't work as written - see the bottom section.
I suggest a simpler approach:
Use a thread job to create the controls in the background, via Start-ThreadJob
.
Start-ThreadJob
is part of the the ThreadJob
module that offers a lightweight, thread-based alternative to the child-process-based regular background jobs and is also a more convenient alternative to creating runspaces via the PowerShell SDK.
It comes with PowerShell [Core] v6+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser
.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
Show your form non-modally (.Show()
rather than .ShowDialog()
) and process GUI events in a [System.Windows.Forms.Application]::DoEvents()
loop.
- Note:
[System.Windows.Forms.Application]::DoEvents()
can be problematic in general (it is essentially what the blocking .ShowDialog()
call does behind the scenes), but in this constrained scenario (assuming only one form is to be shown) it should be fine. See this answer for background information.
In the loop, check for newly created buttons as output by the thread job, attach an event handler, and add them to your form.
Here is a working example that adds 3 buttons to the form after making it visible, one after the other while sleeping in between:
Add-Type -ea Stop -Assembly System.Windows.Forms
$Main = New-Object System.Windows.Forms.Form
# Start a thread job that will create the buttons.
$job = Start-ThreadJob {
$top = 0
1..3 | % {
# Create and output a button object.
($btn = [System.Windows.Forms.Button] @{
Name = "Button$_"
Text = "Button$_"
Top = $top
})
Start-Sleep 1
$top += $btn.Height
}
}
# Show the form asynchronously
$Main.Show()
# Process GUI events in a loop, and add
# buttons to the form as they're being created
# by the thread job.
while ($Main.Visible) {
[System.Windows.Forms.Application]::DoEvents()
if ($button = Receive-Job -Job $job) {
# Add an event handler...
$button.add_Click({ Write-Host "Button clicked: $($this.Name)" })
# .. and it to the form.
$Main.Controls.AddRange($button)
}
# Sleep a little, to avoid a near-tight loop.
# Note: [Threading.Thread]::Sleep() is used in lieu of Start-Sleep,
# to avoid the problem reported in https://github.com/PowerShell/PowerShell/issues/19796
[Threading.Thread]::Sleep(50)
}
# Clean up.
$Main.Dispose()
Remove-Job -Job $job -Force
'Done'
As of this writing, your own answer tries to achieve adding the controls to the form in the original runspace by using Register-ObjectEvent
to subscribe to the other thread's (runspace's) events, given that the -Action
script block used for event handling runs (in a dynamic module inside) the original thread (runspace), but there are two problems with that:
Unlike your answer suggests, the -Action
script block neither directly sees the $Main
variable from the original runspace, nor the other runspace's variables - these problems can be overcome, however, by passing $Main
to Register-ObjectEvent
via -MessageData
and accessing it via $Event.MessageData
in the script block, and by accessing the other runspace's variables via $Sender.Runspace.SessionStateProxy.GetVariable()
calls.
More importantly, however, the .ShowDialog()
call will block further processing; that is, your events won't fire and therefore your -Action
script block won't be invoked until after the form closes.