Search code examples
powershellwinformseventsipcrunspace

Runspace for button event in powershell


$syncHash button event with a separate runspace

Not sure if this is a duplicate, checked online, and worked with what I found, Working with Boe Prox's solutions, which from another StackOverflow article references (https://stackoverflow.com/a/15502286/1546559), but in his, he is updating from a command line/powershell window, via a function run inside a thread. I'm running an event from a button, inside of a thread and trying to run a separate thread, for the click event(s). Outside of the thread, the event works fine, but inside, it doesn't work, at all.. What am I doing wrong? PS. I found another blog referencing Boe Prox's work (https://www.foxdeploy.com/blog/part-v-powershell-guis-responsive-apps-with-progress-bars.html), building another multi-threaded application, but pretty much the same concept, updating a window, through powershell commandlet/function, placed inside of a separate thread.

$push.Add_Click{
    $newRunspace =[runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"         
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
    $powershell = [powershell]::Create().AddScript({           
        $choice = $comboBox.SelectedItem
        # $drive = Get-Location
        if(!(Test-Path -PathType Container -Path "L:\$choice"))
        {
    #        New-Item -ItemType "container" -Path . -Name $choice
            New-Item -ItemType "Directory" -Path . -Name $choice
        }

    #        $folder = $_
            # Where is it being stored at?
            [System.IO.File]::ReadLines("Y:\$choice\IPs.txt") | foreach {
                ping -a -n 2 -w 2000 $_ | Out-Null
                Test-Connection -Count 2 -TimeToLive 2 $_ | Out-Null

                if($?)
                {
                   RoboCopy /Log:"L:\$folder\$_.log" $source \\$_\c$\tools
                   RoboCopy /Log+:"L:\$folder\$folder-MovementLogs.log" $source \\$_\c$\tools
                   Start-Process "P:\psexec.exe" -ArgumentList "\\$_ -d -e -h -s cmd /c reg import C:\tools\dump.reg"
                   # Copy-Item -LiteralPath Y:\* -Destination \\$_\c$\tools
                   $listBox.Items.Add($_)
                }
            }
    })
    $powershell.Runspace = $newRunspace
    $powershell.BeginInvoke()

}


Solution

  • You can use this as a blueprint of what you want to do, the important thing is that the runspace can't see the controls of your form. If you want your runspace to interact with the controls, they must be passed to it, either by SessionStateProxy.SetVariable(...) or as argument with .AddParameters(..) for example.

    using namespace System.Windows.Forms
    using namespace System.Drawing
    using namespace System.Management.Automation.Runspaces
    
    Add-Type -AssemblyName System.Windows.Forms
    
    [Application]::EnableVisualStyles()
    
    try {
        $form = [Form]@{
            StartPosition   = 'CenterScreen'
            Text            = 'Test'
            WindowState     = 'Normal'
            MaximizeBox     = $false
            ClientSize      = [Size]::new(200, 380)
            FormBorderStyle = 'Fixed3d'
        }
    
        $listBox = [ListBox]@{
            Name       = 'myListBox'
            Location   = [Point]::new(10, 10)
            ClientSize = [Size]::new(180, 300)
        }
        $form.Controls.Add($listBox)
    
        $runBtn = [Button]@{
            Location   = [Point]::new(10, $listBox.ClientSize.Height + 30)
            ClientSize = [Size]::new(90, 35)
            Text       = 'Click Me'
        }
        $runBtn.Add_Click({
            $resetBtn.Enabled = $true
    
            if($status['AsyncResult'].IsCompleted -eq $false) {
                # we assume it's running
                $status['Instance'].Stop()
                $this.Text = 'Continue!'
                return # end the event here
            }
    
            $this.Text = 'Stop!'
            $status['Instance']    = $instance
            $status['AsyncResult'] = $instance.BeginInvoke()
        })
        $form.Controls.Add($runBtn)
    
        $resetBtn =  [Button]@{
            Location   = [Point]::new($runBtn.ClientSize.Width + 15, $listBox.ClientSize.Height + 30)
            ClientSize = [Size]::new(90, 35)
            Text       = 'Reset'
            Enabled    = $false
        }
        $resetBtn.Add_Click({
            if($status['AsyncResult'].IsCompleted -eq $false) {
                $status['Instance'].Stop()
            }
            $runBtn.Text  = 'Start!'
            $this.Enabled = $false
            $listBox.Items.Clear()
        })
        $form.Controls.Add($resetBtn)
    
        $status = @{}
        $rs = [runspacefactory]::CreateRunspace([initialsessionstate]::CreateDefault2())
        $rs.ApartmentState = [Threading.ApartmentState]::STA
        $rs.ThreadOptions  = [PSThreadOptions]::ReuseThread
        $rs.Open()
        $rs.SessionStateProxy.SetVariable('controls', $form.Controls)
        $instance = [powershell]::Create().AddScript({
            $listBox = $controls.Find('myListBox', $false)[0]
            $ran  = [random]::new()
    
            while($true) {
                Start-Sleep 1
                $listBox.Items.Add($ran.Next())
            }
        })
        $instance.Runspace = $rs
        $form.Add_Shown({ $this.Activate() })
        $form.ShowDialog()
    }
    finally {
        ($form, $instance, $rs).ForEach('Dispose')
    }
    

    Demo

    demo