Adding elements to a form from another runspace

僤鯓⒐⒋嵵緔 提交于 2021-02-08 04:08:09

问题


I have a form in which as soon as ready several elements will be added (for example, a list). It may take some time to add them (from fractions of a second to several minutes). Therefore, I want to add processing to a separate thread (child). The number of elements is not known in advance (for example, how many files are in the folder), so they are created in the child stream. When the processing in the child stream ends, I want to display these elements on the main form (before that the form did not have these elements and performed other tasks).

However, I am faced with the fact that I cannot add these elements to the main form from the child stream. I will give a simple example as an example. It certainly works:

$Main = New-Object System.Windows.Forms.Form

$Run = {

    # The form is busy while adding elements (buttons here)

    $Top = 0
    1..5 | % {

        $Button = New-Object System.Windows.Forms.Button
        $Button.Top = $Top        
        $Main.Controls.Add($Button)
        $Top += 30
        Sleep 1        
    }
}

$Main.Add_Shown($Run)

# Adding and performing other tasks on the form here

[void]$Main.ShowDialog()

But, adding the same thing to the child stream I did not get the button to display on the main form. I do not understand why.

$Main = New-Object System.Windows.Forms.Form

$Run = {

    $RS = [Runspacefactory]::CreateRunspace()
    $RS.Open()
    $RS.SessionStateProxy.SetVariable('Main', $Main)
    $PS = [PowerShell]::Create().AddScript({

        # Many items will be added here. Their number and processing time are unknown in advance
        # Now an example with the addition of five buttons.

        $Top = 0        
        1..5 | % {

            $Button = New-Object System.Windows.Forms.Button
            $Button.Top = $Top        
            $Main.Controls.Add($Button)
            $Top += 30
            Sleep 1        
        }   
    })

    $PS.Runspace = $RS; $Null = $PS.BeginInvoke()
}

$Main.Add_Shown($Run)

[void]$Main.ShowDialog()

How can I add elements to the main form that are created in the child stream? thanks


回答1:


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)
    }
}

# 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.

    • Update: You mention a workaround in order to get PowerShell's events to fire while the form is being displayed:

      • Subscribe to the MouseMove event with a dummy event handler whose invocation gives PowerShell a chance to fire its own events while the form is being displayed modally; e.g.: $Main.Add_MouseMove({ Out-Host }); note that this workaround is only effective if the script block calls a command, such as Out-Host in this example (which is effectively a no-op); a mere expression or .NET method call is not enough.

      • However, this workaround is suboptimal in that it relies on the user (continually) mousing over the form for the PowerShell events to fire; also, it is somewhat obscure and inefficient.




回答2:


I think you can't create form and controls on different threads. But you can access control properties though. So you can create form with control placeholders in a runspace, then change them on the main thread once your calculations are complete. Example:

$form = New-Object System.Windows.Forms.Form

$rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = [System.Threading.ApartmentState]::MTA
$ps = [powershell]::create()
$ps.Runspace = $rs 
$rs.Open()

$out = $ps.AddScript({param($form)

  $button1 = New-Object System.Windows.Forms.Button
  $button1.Name = "button1"
  $form.Controls.Add($button1) 
  $form.ShowDialog()

}).AddArgument($form).BeginInvoke()


#-----------------------------


sleep 1; 


$form.Controls["button1"].Text = "some button"



回答3:


This is the way I'm using now. Thanks to @mklement0 for the talk about the Register-ObjectEvent method (here). I applied it here. The essence of the method is that the elements are created in the child stream (in this case, the Button), and when the child space has finished work, Register-ObjectEvent is processed. Register-ObjectEvent is located in the main space and therefore allows you to add an element (Button here) to the form.

$Main = New-Object System.Windows.Forms.Form

$Run = {

    $RS = [Runspacefactory]::CreateRunspace()
    $RS.Open()
    $RS.SessionStateProxy.SetVariable('Main', $Main)
    $PS = [PowerShell]::Create().AddScript({        

            $Button = New-Object System.Windows.Forms.Button
        }   
    })

    $PS.Runspace = $RS

    $Null = Register-ObjectEvent -InputObject $PS -EventName InvocationStateChanged -Action {    
        if ($EventArgs.InvocationStateInfo.State -in 'Completed', 'Failed') {
            $Main.Controls.Add($Button)
        }
    }

    $Null = $PS.BeginInvoke()
}

$Main.Add_Shown($Run)

[void]$Main.ShowDialog()

This is my a workaround. However, I still do not know if it is possible in principle to add elements of a child space to a form from a child space. This, of course, is about adding, not managing, because managing from the child space is successful.




回答4:


Putting this here, since it is too long for the regular comment section.

Now, I don't spend much time using runspaces in production, as I've no had a real need (at least to date) for them, in class, sure, but I digress.

However, from all my previous readings and notes I've kept, this sounds like a use case for RunSpace Pools. Here are three of my saved resources. Working under the assumption that you may have not seen all of them of course. Now, I would post their code as well, but all are very long, so, there's that. Based on your use case, it could be seen as a duplicate to the last link resource.

PowerShell and WPF: Writing Data to a UI From a Different Runspace

PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI Applications

Sharing Variables and Live Objects Between PowerShell Runspaces

How to access a different powershell runspace without WPF-object



来源:https://stackoverflow.com/questions/60127351/adding-elements-to-a-form-from-another-runspace

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!