Search code examples
powershellwinformsforeachclosures

PowerShell Add_Click in foreach loop


What I am trying to accomplish is to create buttons that launch exe files in a certain directory when clicked, but when I try using a foreach loop to create a few buttons, all of the buttons just launch the file the last button is supposed to launch.

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

$form = New-Object System.Windows.Forms.Form
$form.Text = 'Main Window'
$form.Size = New-Object System.Drawing.Size(600,400)

$flp = New-Object System.Windows.Forms.FlowLayoutPanel
$flp.Location = New-Object System.Drawing.Point(0,0)
$flp.Height = $form.Height
$flp.Width = $form.Width
$form.Controls.Add($flp)

$files = Get-ChildItem "$home\Downloads" -Include *.exe -Name

foreach ($file in $files){
    $button = New-Object System.Windows.Forms.Button
    $flp.Controls.Add($button)
    $button.Width = 100
    $button.Height = 50
    $button.Text = $file
    $button.Add_Click{
        Start-Process -FilePath "$home\Downloads\$file"
    }
}

$form.Topmost = $true
$form.ShowDialog()

Whatever I'm doing is probably pretty stupid, so I was just looking for any alternatives or solutions to this other than to just hard code everything.


Solution

  • It is likely that you need to use .GetNewClosure() ScriptBlock method so that each script block (button click event) holds the current value of the $file variable at the moment of enumeration.

    Example of what this means:

    $blocks = foreach($i in 0..5) {
        { "hello $i" }
    }
    & $blocks[0] # => hello 5
    & $blocks[1] # => hello 5
    
    $blocks = foreach($i in 0..5) {
        { "hello $i" }.GetNewClosure()
    }
    & $blocks[0] # => hello 0
    & $blocks[1] # => hello 1
    

    In that sense, and assuming this is the issue, the following should work:

    foreach ($file in $files) {
        $button = [System.Windows.Forms.Button]@{
            Width  = 100
            Height = 50
            Text   = $file
        }
        $button.Add_Click(
            { Start-Process -FilePath "$home\Downloads\$file" }.GetNewClosure())
        $flp.Controls.Add($button)
    }
    

    A nice alternative to having a need to use .GetNewClosure() can be seen on this answer. The .Tag property of the Button can be used to store the information of the file's path which then can be used on the button's .Click event:

    foreach ($file in $files) {
        $button = [System.Windows.Forms.Button]@{
            Width  = 100
            Height = 50
            Text   = $file
            Tag    = "$home\Downloads\$file"
        }
        $button.Add_Click({ Start-Process -FilePath $this.Tag })
        $flp.Controls.Add($button)
    }