powershellwinformsasynchronouseventsrunspace

How to pass a variable with the current value to runspace?


I'm writing a script that will download the selected programs I need (thank you Santiago Squarzon for your help). The script works well when I need to download one program, but to download several in a row I need to start the next one at the end of the download. I made a list of downloads $FileList and I want to pass its value to runspace in order to start downloading the next program (and remove the already downloaded one from the list). And so on until I go through the entire list. There will be more programs later. For now, two are enough for the test.

Add-Type -assembly System.Windows.Forms

$webMain = New-Object System.Net.WebClient
$FileList = @()

$MainForm = New-Object System.Windows.Forms.Form
$MainForm.Width = 420
$MainForm.Height = 200
$MainForm.FormBorderStyle = "Fixed3d"
$MainForm.MaximizeBox = $false
$MainForm.StartPosition = "CenterScreen"

$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size(120,50)
$Button1.Size = New-Object System.Drawing.Size(160,50)
$Button1.Text = "Download selected"
$Button1.Name = "Button1"
$Button1.Add_Click({
    $Button1.Enabled = $false
    if ($CheckBox1.Checked) {
        $FileList += @{Link = "https://dl.google.com/chrome/install/standalonesetup64.exe"; Path = "D:\chrome.exe"}
    }
    if ($CheckBox2.Checked) {
        $FileList += @{Link = "https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=ru"; Path = "D:\firefox.exe"}
    }
    $FileList = [System.Collections.ArrayList]$FileList
    Wait-Event -Timeout 5
    if ($FileList.count -gt 0) {
        $webMain.DownloadFileAsync($FileList[0].Link, $FileList[0].Path)
        $FileList.Remove($FileList[0])
    }
    
})
$MainForm.Controls.Add($Button1)

$CheckBox1 = New-Object System.Windows.Forms.CheckBox
$CheckBox1.Location = New-Object System.Drawing.Size(10,10)
$CheckBox1.Size = New-Object System.Drawing.Size(100,20)
$CheckBox1.Text = "Chrome"
$MainForm.Controls.Add($CheckBox1)

$CheckBox2 = New-Object System.Windows.Forms.CheckBox
$CheckBox2.Location = New-Object System.Drawing.Size(10,30)
$CheckBox2.Size = New-Object System.Drawing.Size(100,20)
$CheckBox2.Text = "Firefox"
$MainForm.Controls.Add($CheckBox2)

$ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
$ProgressBar1.Location = New-Object System.Drawing.Size(5,120)
$ProgressBar1.Size = New-Object System.Drawing.Size(400,40)
$ProgressBar1.Name = "ProgressBar1"
$MainForm.Controls.Add($ProgressBar1)

$rs = [runspacefactory]::CreateRunspace($Host)
$rs.Open()
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm))
$rs.SessionStateProxy.PSVariable.Set([psvariable]::new('FileList', $FileList))

$ps = [powershell]::Create().AddScript({

    Register-ObjectEvent -InputObject $args[0] -EventName 'DownloadProgressChanged' -SourceIdentifier 'WebMainDownloadProgressChanged' -Action {
        [System.Threading.Monitor]::Enter($form)
        $progress = $form.Controls.Find('ProgressBar1', $false)[0]
        $progress.Value = $eventArgs.ProgressPercentage
        [System.Threading.Monitor]::Exit($form)
    }
     
    Register-ObjectEvent -InputObject $args[0] -EventName 'DownloadFileCompleted' -SourceIdentifier 'WebMainDownloadFileCompleted' -Action {
        [System.Threading.Monitor]::Enter($form)
        $progress = $form.Controls.Find('ProgressBar1', $false)[0]
        $progress.Value = 0
        [System.Threading.Monitor]::Exit($form)
        if ($FileList.count -gt 0) {
            $webMain.DownloadFileAsync($FileList[0].Link, $FileList[0].Path)
            $FileList.Remove($FileList[0])
        }
        if ($FileList.count -eq 0) {
            $form.Controls.Find('Button1', $false)[0].Enabled = $true
        }
    }

}).AddArgument($webMain)

$ps.Runspace = $rs
$task = $ps.BeginInvoke()
$MainForm.ShowDialog()

As far as I understand, $rs.SessionStateProxy.PSVariable.Set([psvariable]::new('FileList', $FileList)) transmits the current value at the time the script starts, and not at the time the button is pressed. How can I correctly pass the $FileList variable to runspace so that it contains the current value at the time the button is pressed?


Solution

  • Although I still haven’t figured out why $rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm)) transmits the "current" form with which you can work, and $rs.SessionStateProxy.PSVariable.Set ([psvariable]::new('FileList', $FileList)) transmits only the value of the variable at the moment the script is launched and does not “update” it when changed, but I realized that you can simply transfer all processing to that separate runspace and then there is no need will transmit nothing.

    This is what I ended up with:

    Add-Type -assembly System.Windows.Forms
    
    $MainForm = New-Object System.Windows.Forms.Form
    $MainForm.Width = 420
    $MainForm.Height = 200
    $MainForm.FormBorderStyle = "Fixed3d"
    $MainForm.MaximizeBox = $false
    $MainForm.StartPosition = "CenterScreen"
    
    $Button1 = New-Object System.Windows.Forms.Button
    $Button1.Location = New-Object System.Drawing.Size(120,50)
    $Button1.Size = New-Object System.Drawing.Size(160,50)
    $Button1.Text = "Download selected"
    $Button1.Name = "Button1"
    $MainForm.Controls.Add($Button1)
    
    $CheckBox1 = New-Object System.Windows.Forms.CheckBox
    $CheckBox1.Location = New-Object System.Drawing.Size(10,10)
    $CheckBox1.Size = New-Object System.Drawing.Size(100,20)
    $CheckBox1.Text = "Chrome"
    $CheckBox1.Name = "CheckBox1"
    $MainForm.Controls.Add($CheckBox1)
    
    $CheckBox2 = New-Object System.Windows.Forms.CheckBox
    $CheckBox2.Location = New-Object System.Drawing.Size(10,30)
    $CheckBox2.Size = New-Object System.Drawing.Size(100,20)
    $CheckBox2.Text = "Firefox"
    $CheckBox2.Name = "CheckBox2"
    $MainForm.Controls.Add($CheckBox2)
    
    $ProgressBar1 = New-Object System.Windows.Forms.ProgressBar
    $ProgressBar1.Location = New-Object System.Drawing.Size(5,120)
    $ProgressBar1.Size = New-Object System.Drawing.Size(400,40)
    $ProgressBar1.Name = "ProgressBar1"
    $MainForm.Controls.Add($ProgressBar1)
    
    $rs = [runspacefactory]::CreateRunspace($Host)
    $rs.Open()
    $rs.SessionStateProxy.PSVariable.Set([psvariable]::new('form', $MainForm))
    
    $ps = [powershell]::Create().AddScript({
    
        $FileList = @()
        $webMain = New-Object System.Net.WebClient
        $Button1 = $form.Controls.Find('Button1', $false)[0]
    
        $Button1.Add_Click({
            $Button1.Enabled = $false
            if ($form.Controls.Find('CheckBox1', $false)[0].Checked) {
                $Global:FileList += @{Link = "https://dl.google.com/chrome/install/standalonesetup64.exe"; Path = "D:\chrome.exe"}
            }
            if ($form.Controls.Find('CheckBox2', $false)[0].Checked) {
                $Global:FileList += @{Link = "https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=ru"; Path = "D:\firefox.exe"}
            }
            $Global:FileList = [System.Collections.ArrayList]$Global:FileList
            Wait-Event -Timeout 5
            if ($Global:FileList.count -gt 0) {
                $webMain.DownloadFileAsync($Global:FileList[0].Link, $Global:FileList[0].Path)
                $Global:FileList.Remove($Global:FileList[0])
            }
        })
    
        Register-ObjectEvent -InputObject $webMain -EventName 'DownloadProgressChanged' -SourceIdentifier 'WebMainDownloadProgressChanged' -Action {
            [System.Threading.Monitor]::Enter($form)
            $progress = $form.Controls.Find('ProgressBar1', $false)[0]
            $progress.Value = $eventArgs.ProgressPercentage
            [System.Threading.Monitor]::Exit($form)
        }
         
        Register-ObjectEvent -InputObject $webMain -EventName 'DownloadFileCompleted' -SourceIdentifier 'WebMainDownloadFileCompleted' -Action {
            [System.Threading.Monitor]::Enter($form)
            $progress = $form.Controls.Find('ProgressBar1', $false)[0]
            $progress.Value = 0
            if ($Global:FileList.count -eq 0) {
                $form.Controls.Find('Button1', $false)[0].Enabled = $true
            }
            [System.Threading.Monitor]::Exit($form)
            if ($Global:FileList.count -gt 0) {
                $webMain.DownloadFileAsync($Global:FileList[0].Link, $Global:FileList[0].Path)
                $Global:FileList.Remove($FileList[0])
            }
        }
    
    })
    
    $ps.Runspace = $rs
    $ps.BeginInvoke()
    $MainForm.ShowDialog()