1

Second question today but this time I believe there's something really strange going on. The following code is not my original, I tried to reproduce the behavior with as few lines as possible. In my original code I'm importing the application paths and names with a config file and call the CreateButton function in a foreach loop.

I'm trying to create multiple buttons with a function wich should open different applications. When I hardcode the Path everything is working. when I use variables in a foreach loop every button uses the LAST path submitted. Here is code that works:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}

$Command={Start-Process -FilePath "C:\Windows\system32\mmc.exe"}
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Command={Start-Process -FilePath "C:\Windows\regedit.exe"}
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Command={Start-Process -FilePath "C:\Windows\System32\cmd.exe"}
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()

Here is Code that does not work:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}


$Global:Test="C:\Windows\system32\mmc.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Global:Test="C:\Windows\regedit.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Global:Test="C:\Windows\System32\cmd.exe"
$Command={Start-Process -FilePath $Global:Test}
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()

The first example creates a form with 3 buttons. When I click each button different applications get started. The second example creates a form with 3 buttons. When I click each button "CMD.EXE" gets opened. When I change the CMD.EXE with the MMC.EXE block in the code each button will open the MMC. Can anyone explain whats going on?

ddl
  • 29
  • 4

2 Answers2

2

The $Global:Test variable within the $Command blocks doesn't get resolved until the command block executes. I.e., when it creates the command block, it literally has $Global:Test in it, not the value that you set on the prior line.

Accordingly, when a $Command block executes, it then resolves the variable, which is set according to the last $Global:Test= assignment in your script.

Tony Hinkle
  • 4,706
  • 7
  • 23
  • 35
  • Now this makes sense. I would rate your Answer if the system would allow me to. Do you have any idea how I could set the FilePath Argument with a variable in a way that works? My problem is that I can't fire the start-process cmdlet in the btn.add_click() without a Commandblock and I can't think of a way to get it to work with that, now that I know how it works. – ddl Jun 26 '15 at 20:17
2

Essentially you want, that different instance of same script block produce a different results. For that to happen you have to attach different context to each script block instance. One way to do this is to bind script block to module. For that you can use GetNewClosure() script block instance method:

$ScriptBlockWithContext=$ScriptBlock.GetNewClosure()

This method will create new module and capture all current scope variables into it. If you want to have more control about what to be captured, than you can create module by yourself:

$Module=New-Module {$Context=$Global:Context}
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

If you want to refer local variable, then you can pass them as additional parameters to New-Module cmdlet:

$Module=New-Module {param($Context)} $Context
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

Alternatively, you can invoke operator & or . to invoke arbitrary code in module scope after you create it. In particular, you can use New-Variable or Set-Variable cmdlet to set variables inside module scope:

$Module=New-Module {}
& $Module New-Variable Context $Context
$ScriptBlockWithContext=$Module.NewBoundScriptBlock($ScriptBlock)

Here simple modification to your code to make it work:

# Define Form
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object Windows.Forms.Form
$form.Size = New-Object Drawing.Size @(260,250)
$Form.Text = "ToolBox"
$form.StartPosition = "CenterScreen"

# Create Button Function
function CreateButton ($ButtonCommand, $ButtonName, $ButtonLocX, $ButtonLocY, $ButtonSizeX, $ButtonSizeY)
{
$btn = New-Object System.Windows.Forms.Button
$btn.add_click($ButtonCommand)
$btn.Text = $ButtonName
$btn.Location = New-Object System.Drawing.Size($ButtonLocX,$ButtonLocY)
$btn.Size = New-Object System.Drawing.Size($ButtonSizeX,$ButtonSizeY)
$form.Controls.Add($btn)
}

$CaptureCommand={param($Test)}
$CommonCommand={Start-Process -FilePath $Test}

$Module=New-Module $CaptureCommand "C:\Windows\system32\mmc.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='MMC'
$LocationX=20
$LocationY=10
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Module=New-Module $CaptureCommand "C:\Windows\regedit.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='Regedit'
$LocationX=20
$LocationY=70
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

$Module=New-Module $CaptureCommand "C:\Windows\System32\cmd.exe"
$Command=$Module.NewBoundScriptBlock($CommonCommand)
$Text='CMD'
$LocationX=20
$LocationY=100
$SizeX=200
$SizeY=30
CreateButton $Command $Text $LocationX $LocationY $SizeX $SizeY

#Show Form
$drc = $form.ShowDialog()
user4003407
  • 21,204
  • 4
  • 50
  • 60
  • This is working like a charm! I already succesfully implemented this solution in my original code and now I will sit down for an hour and try to understand what this code does ;-). Thank you very much! – ddl Jun 26 '15 at 21:06
  • Thank you again for this great explanation! – ddl Jun 27 '15 at 16:27