'Powershell GUI auto generate buttons with functions

TLDR: How can I make a generated variable, and then call that variable later within a Add_click. I am sure some kind of serialization of each Object/button I make is what is needed.

I am building a small tool that reads from a csv to create a button, and function.

the csv looks something like

Name  Type  Link  Script
Powershell App C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe Empty
FixXYZ Fix Empty -ScriptStuffHere- 

The tool will then make a button with the Name, (work in progress to filter apps and fixes), and when you click the button, if its an app will do start ($link) and if its a fix it will run that script.

My issue is I have it making the button and giving them names, and the name of the button stays, but the function does not.

full code:

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework
[System.Windows.Forms.Application]::EnableVisualStyles()

#=======================================================

$Form                            = New-Object system.Windows.Forms.Form
$Form.text                       = "Form"
$Form.TopMost                    = $false
$Form.ClientSize                 = New-Object System.Drawing.Point(760,400)
$Form.minimumSize                = New-Object System.Drawing.Size(760,400) 
$Form.maximumSize                = New-Object System.Drawing.Size(760,400) 

$GetCSV = import-csv "C:\File.csv"
$Details = $GetCSV.Name

$DeviceList = $GetCSV

$Count = $DeviceList.Lines.Count
$ObjectNumber = -1
Write-Host "Total Entries:" $Count


$x = 0 #up down
$z = 0 #left right


$Names = @($DeviceList.Lines)
$Names | ForEach-Object{
$ObjectNumber += 1
Write-Host "Object:" $ObjectNumber 

$x += 0
$z += 120

if($z -eq 720){
$x += 120
$z = 0
Write-Host "New Row"}



Write-Host "x" $x
Write-Host "z" $z
$ButtonLabel = ($GetCSV[$ObjectNumber]).Name

set-Variable -Name "var$ObjectNumber" -Value ($GetCSV[$ObjectNumber] | Select Name, Type, Link, Script, File, FileSource)

Write-Host "Name: " (Get-Variable -Name "var$ObjectNumber" -ValueOnly).Name
Write-Host "Type: " (Get-Variable -Name "var$ObjectNumber" -ValueOnly).Type
Write-Host "Link: "(Get-Variable -Name "var$ObjectNumber" -ValueOnly).Link
Write-Host "Script: "(Get-Variable -Name "var$ObjectNumber" -ValueOnly).Script
Write-Host "File: "(Get-Variable -Name "var$ObjectNumber" -ValueOnly).File
Write-Host =========================

         
$_                         = New-Object system.Windows.Forms.Button
$_.text                    = $ButtonLabel
$_.width                   = 100
$_.height                  = 100
$_.location                = New-Object System.Drawing.Point($z,$x)
$_.Font                    = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
$_.Add_Click({              Start (Get-Variable -Name "var$ObjectNumber" -ValueOnly).Link})

$Form.Controls.Add($_)

}

[void]$Form.ShowDialog()

I am very certain my issue is coming from $_.Add_Click({Start (Get-Variable -Name "var$ObjectNumber" -ValueOnly).Link})

I know the issue is with $ObjectNumber because that number is getting +1 each time the ForEach is gone through, so when I click a button, its taking "var$OjbectNumber" as its Last number. Clicking the button works, but all buttons open the last entries link.



Solution 1:[1]

The answer was using a unused property to throw my desired call back variable in. So in this case, i have a folder with with programs, the button will be made, and set the $Button.Text (its name) as the name of the .exe, and then it sets the $Button.Tag as the file path, so when I go do the button.Add_Click , I just call the Button.Tag as it will have the path of my Exe.

    Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName PresentationFramework
[System.Windows.Forms.Application]::EnableVisualStyles()
$Form                            = New-Object system.Windows.Forms.Form
$Form.ClientSize                 = '580,400'
$Form.Text                       = "Test"
$Form.TopMost                    = $false
$Form.FormBorderStyle            = 'Fixed3D'
$Form.MaximizeBox                = $false
$Form.minimumSize                = New-Object System.Drawing.Size(580,400) 
$Form.maximumSize                = New-Object System.Drawing.Size(580,400) 

#Place Holder Form Junk Above




#Reset these on Run
$Global:x             = 10 #Reset up down
$Global:z             = 10 #Reset left right
$Global:ObjectNumber = -1 #Reset Object Count






Function Make-Button([string] $ToolName, [string] $ToolPath, [string] $SetZ, [string] $SetX){
$Button                         = New-Object system.Windows.Forms.Button
$Button.text                    = $ToolName
$Button.width                   = 120
$Button.height                  = 120
$Button.location                = New-Object System.Drawing.Point($SetZ,$SetX)
$Button.Font                    = New-Object System.Drawing.Font('Franklin Gothic',10)
$Button.FlatStyle           = [System.Windows.Forms.FlatStyle]::Flat
$Button.FlatAppearance.BorderSize  = 0
$Button.ForeColor           = [System.Drawing.ColorTranslator]::FromHtml("#ffffff")
$Button.BackColor           = [System.Drawing.ColorTranslator]::FromHtml("#515582")

$Button.tag = $ToolPath #<- this is where the answer was. Throwing my desired callback into an unused property of the the Button. in this case, i used _.Tag
$Button.Add_Click{start $this.tag}

$Form.Controls.AddRange(@($Button))

Write-Host "$ToolName"
Write-Host "$ToolPath"
Write-Host "$SetZ"
Write-Host "$SetX"

}

function Get-Position{
switch ($Global:ObjectNumber) {
-1{$Global:ObjectNumber += 1
Write-Host "Object:" $Global:ObjectNumber 

$Global:x = 0
$Global:z += 0}


Default{$Global:ObjectNumber += 1
Write-Host "Object:" $Global:ObjectNumber 

$Global:x += 0
$Global:z += 140}
}#end switch

if($Global:z -eq 570){ #Make New Row
$Global:x += 140
$Global:z = 10
Write-Host "New Row"
}
}


$Tools = Get-ChildItem "C:\WINDOWS\system32" -Filter *.exe
$Count = ( $Tools | Measure-Object ).Count;
Write-Host "Entries:" $Count





$Names = @($Tools) #Put Tools in Array
$Names | ForEach-Object{

                        Get-Position
                        Make-Button ($_.Name).replace(".exe","") ($_.FullName) ($z) ($x)

}











#End Form
$Test.Add_Shown(            {$Test.Activate()})
$Test.ShowDialog() 
[void]$Form.ShowDialog()

Solution 2:[2]

Continuing from my comment...

A small refactor to get this to show where things are

Clear-Host 

Add-Type -AssemblyName System.Windows.Forms,
                       PresentationFramework

[System.Windows.Forms.Application]::EnableVisualStyles()

$Form             = New-Object system.Windows.Forms.Form
$Form.text        = 'Form'
$Form.TopMost     = $false
$Form.ClientSize  = New-Object System.Drawing.Point(760,400)
$Form.minimumSize = New-Object System.Drawing.Size(760,400) 
$Form.maximumSize = New-Object System.Drawing.Size(760,400) 

$GetCSV       = Import-Csv -LiteralPath 'D:\Scripts\File.csv'
$Details      = $GetCSV.Name
$DeviceList   = $GetCSV
$Count        = $DeviceList.Count
$ObjectNumber = -1
 "Total Entries: $Count`n`n"

$ObjDown  = 0
$ObjRight = 0

$DeviceList.Name | 
ForEach-Object{
    $ObjectNumber += 1
    "`nObject: $ObjectNumber"

    $x        = 0
    $ObjRight = 120

    if($ObjRight -eq 720)
    {
        $x        = 120
        $ObjRight = 0
        'New Row'
    }


    "x $x"
    "z $ObjRight"

    $ButtonLabel = ($GetCSV[$ObjectNumber]).Name

    set-Variable -Name $("var$ObjectNumber") -Value ($GetCSV[$ObjectNumber] | 
    Select Name, Type, Link, Script, File, FileSource)

    ("Name:   $((Get-Variable -Name $("var$ObjectNumber") -ValueOnly).Name)")
    ("Type:   $((Get-Variable -Name $("var$ObjectNumber") -ValueOnly).Type)")
    ("Link:   $((Get-Variable -Name $("var$ObjectNumber") -ValueOnly).Link)")
    ("Script: $((Get-Variable -Name $("var$ObjectNumber") -ValueOnly).Script)")
    ("File:   $((Get-Variable -Name $("var$ObjectNumber") -ValueOnly).File)")
         
    $PSitem          = New-Object system.Windows.Forms.Button
    $PSitem.text     = $ButtonLabel
    $PSitem.width    = 100
    $PSitem.height   = 100
    $PSitem.location = New-Object System.Drawing.Point($ObjRight,$x)
    $PSitem.Font     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)

    $PSitem.Add_Click({
        $(Get-Variable -Name $("var$ObjectNumber") -ValueOnly)
    })
    $Form.Controls.Add($PSitem)
}

#[void]$Form.ShowDialog()

Here is an example I gave as an answer to another post to dynamically create UX/UI elements and assign a form event, though not using an external file, it's the same concept.

How to create multiple button with PowerShell?

Add tooltip and form event, like so...

$Form                            = New-Object system.Windows.Forms.Form
$Form.ClientSize                 = New-Object System.Drawing.Point(381,316)
$Form.text                       = "Auto Button UI"
$Form.TopMost                    = $false
$Form.BackColor                  = [System.Drawing.ColorTranslator]::FromHtml("#c9f6fe")

$i = 0
Get-Variable -Name 'Button*' | 
Remove-Variable

$objTooltip = New-Object System.Windows.Forms.ToolTip 
$objTooltip.InitialDelay = 100 

1..3 | 
foreach{
    $CurrentButton          = $null
    $CurrentButton          = New-Object System.Windows.Forms.Button
    $CurrentButton.Location = "$(50+100*$i), 275"
    $CurrentButton.Text     = $PSItem
    $CurrentButton.Font     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
    
    New-Variable "Button$PSitem" $CurrentButton

    $objTooltip.SetToolTip(
        $CurrentButton, 
        "Execute action assigned to $($CurrentButton.Text)"
    )

    $CurrentButton.add_click(
    {
        [System.Windows.Forms.MessageBox]::
        Show(
            "$($CurrentButton.Text)", $($CurrentButton.Text), [System.Windows.Forms.MessageBoxButtons]::
            OKCancel, [System.Windows.Forms.MessageBoxIcon]::Information
        )
    })

    $i++
    $form.Controls.Add($CurrentButton)
}

[void]$Form.ShowDialog()

Yet, though it adds the event to each button element, the message text is the last one passed. Unless explicitly called as in the example from the link.

Solution 3:[3]

To adapt the second example in the answer already provided here so that the message text is not just the last one passed, you can change the reference within the event to the instance this.text rather than the iteratively updated $CurrentButton.text

    $CurrentButton.add_click(
    {
        [System.Windows.Forms.MessageBox]::
        Show(
            "$($this.Text)", $($this.Text), [System.Windows.Forms.MessageBoxButtons]::
            OKCancel, [System.Windows.Forms.MessageBoxIcon]::Information
        )
    })

Credit to jrv https://social.technet.microsoft.com/Forums/ie/en-US/09ff4141-6222-4bff-b8a9-a1253e0d378a/powershell-form-procedurally-creating-buttons?forum=ITCG

Full code with serialization of button object and event:

Clear-Host 

Add-Type -AssemblyName System.Windows.Forms,
                       PresentationFramework

[System.Windows.Forms.Application]::EnableVisualStyles()

$Form                            = New-Object system.Windows.Forms.Form
$Form.ClientSize                 = New-Object System.Drawing.Point(381,316)
$Form.text                       = "Auto Button UI"
$Form.TopMost                    = $false
$Form.BackColor                  = [System.Drawing.ColorTranslator]::FromHtml("#c9f6fe")

$i = 0
Get-Variable -Name 'Button*' | 
Remove-Variable

$objTooltip = New-Object System.Windows.Forms.ToolTip 
$objTooltip.InitialDelay = 100 

1..3 | 
foreach{
    $CurrentButton          = $null
    $CurrentButton          = New-Object System.Windows.Forms.Button
    $CurrentButton.Location = "$(50+100*$i), 275"
    $CurrentButton.Text     = $PSitem
    $CurrentButton.Font     = New-Object System.Drawing.Font('Microsoft Sans Serif',10)
    
    New-Variable "Button$PSitem" $CurrentButton

    $objTooltip.SetToolTip(
        $CurrentButton, 
        "Execute action assigned to $($CurrentButton.Text)"
    )

    $CurrentButton.add_click(
    {
        [System.Windows.Forms.MessageBox]::
        Show(
            "$($this.Text)", $($this.Text), [System.Windows.Forms.MessageBoxButtons]::
            OKCancel, [System.Windows.Forms.MessageBoxIcon]::Information
        )
    })

    $i++
    $form.Controls.Add($CurrentButton)
}

[void]$Form.ShowDialog()

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 JakeJigsaw
Solution 2 postanote
Solution 3