1

I’m rather new at PowerShell and am currently unable to find another (better?) solution for the following issue:

This is my code: (testcase)

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

function Add-Item ([string]$fItem) {
    Write-Host "Do black magic with $fItem here"
}

$MyListOfItems=@()
$MyListOfItems+="foo"
$MyListOfItems+="bar"
$MyListOfItems+="foobar"
$MyListOfItems+="barfoo"
$MyListOfItems+="boofar"

$Form = New-Object System.Windows.Forms.Form 
$Form.AutoSize = $True
$Form.AutoSizeMode = "GrowAndShrink"
$Form.Add_Shown({$Form.Activate()})

$Counter=0
ForEach ($Item in $MyListOfItems) {
    $AddButton = New-Object System.Windows.Forms.Button
    $AddButton.Size = New-Object System.Drawing.Size(100,23)
    $AddButton.Location = New-Object System.Drawing.Point(10,(10+25*$Counter))
    $AddButton.Text = $Item
    #$Command = "Add-Item $Item"
    #$AddButton.Add_Click({ Invoke-Expression -command $Command }.GetNewClosure())
    $AddButton.Add_Click({ Add-Item $Item }.GetNewClosure())
    #$AddButton.Add_Click({ Add-Item $Item })
    $Form.Controls.Add($AddButton)
    $Counter++
}


[void]$Form.ShowDialog()

In PowerShell ISE it runs perfectly:

Do black magic with foo here
Do black magic with bar here
Do black magic with foobar here
Do black magic with barfoo here
Do black magic with boofar here

On the other hand, When I run it with Powershell.exe (read: “Run with Powershell” context menu) I get the error on each of the buttons:

Add-Item : The term 'Add-Item' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At C:\Scripts\buttontest.ps1:62 char:24
+ $AddButton.Add_Click({ Add-Item $Item }.GetNewClosure())
+                        ~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Add-Item:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

If I change the Add_Click event for the button to:

$AddButton.Add_Click({ Add-Item $Item })

I don’t get an error but arrive at the common pitfall of getting only the last item back for each button pressed:

Do black magic with boofar here
Do black magic with boofar here
Do black magic with boofar here
Do black magic with boofar here
Do black magic with boofar here

I managed to get this working by changing the scope of the function to the global scope. While this does work, this doesn’t sit well with me. The modified function:

function global:Add-Item ([string]$fItem) {
    Write-Host "Do black magic with $fItem here"
}

Is there some way to get this code working while avoiding the use of a global scope for my function? I've searched several forums and found many topics on different ways to implement an add_click event and differences between Powershell ISE and commandline and but was unable to find an answer between them

Note: While I'm developing this in a PowerShell 3 environment, this still needs to be compatible with PowerShell 2

Thanks in advance, Wim.

  • "In PowerShell ISE it runs perfectly:" - Which version of the ISE precisely? The "Run with Powershell" context menu is launching a specific version of Powershell, so which version, is it launching? – Ramhound Oct 30 '15 at 11:50
  • $PSVersionTable shows the same info in both ISE, “Run with PowerShell” and PowerShell CLI: PSversion=3.0 / WSManStackVersion=3.0 / SerializationVersion=1.1.0.1 / PSCompatibleVersions={1.0, 2.0, 3.0} / PSRemotingProtocolVersion=2.2 / $PSVersiontable.BuildVersion=6.2.9200.17065 For PowerShell ISE I could find no Help/About menu item with clear version info. Property details of the powershell_ise.EXE show version 6.2.9200.16434 and F1 Help talks about “PowerShell ISE for Windows Server 2012” (even though I’m on a Windows 8 machine [not 8.1]) I hope that is roughly what you were asking? – GapWim Oct 30 '15 at 12:47
2

The difference between ISE and “Run with Powershell” is that ISE run script in global scope, while “Run with Powershell” create new scope for script. And code from other modules (GetNewClosure create new module and bound script block to it) can see global scope, but can not see nested scopes. As a solution, you can create module by yourself and define Add-Item function inside that module:

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

$MyListOfItems=@(
    "foo"
    "bar"
    "foobar"
    "barfoo"
    "boofar"
)

$Form = New-Object System.Windows.Forms.Form 
$Form.AutoSize = $True
$Form.AutoSizeMode = "GrowAndShrink"
$Form.Add_Shown({$Form.Activate()})

$Counter=0
ForEach ($Item in $MyListOfItems) {
    $AddButton = New-Object System.Windows.Forms.Button
    $AddButton.Size = New-Object System.Drawing.Size(100,23)
    $AddButton.Location = New-Object System.Drawing.Point(10,(10+25*$Counter))
    $AddButton.Text = $Item
    $Module = New-Module {
        param($Item)
        function Add-Item ([string]$fItem) {
            Write-Host "Do black magic with $fItem here"
        }
    } $Item -Function @()
    $AddButton.Add_Click($Module.NewBoundScriptBlock({ Add-Item $Item }))
    $Form.Controls.Add($AddButton)
    $Counter++
}

[void]$Form.ShowDialog()

You can also use different approach and save related data in Tag property of Control:

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

function Add-Item ([string]$fItem) {
    Write-Host "Do black magic with $fItem here"
}

$MyListOfItems=@(
    "foo"
    "bar"
    "foobar"
    "barfoo"
    "boofar"
)

$Form = New-Object System.Windows.Forms.Form 
$Form.AutoSize = $True
$Form.AutoSizeMode = "GrowAndShrink"
$Form.Add_Shown({$Form.Activate()})

$Counter=0
ForEach ($Item in $MyListOfItems) {
    $AddButton = New-Object System.Windows.Forms.Button
    $AddButton.Size = New-Object System.Drawing.Size(100,23)
    $AddButton.Location = New-Object System.Drawing.Point(10,(10+25*$Counter))
    $AddButton.Text = $Item
    $AddButton.Tag = @{Item = $Item}
    $AddButton.Add_Click({param($sender) Add-Item $sender.Tag.Item })
    $Form.Controls.Add($AddButton)
    $Counter++
}

[void]$Form.ShowDialog()
  • Thank you for your reply, this does indeed work. One question though: the code I shared was a simplified test case of what I’m trying to do. In reality the Add-Item function turn out to be quite hefty. And depending on the database involved there can be a lot of buttons. Isn’t creating a new module for each button putting a lot of load on memory/cpu? Also, selecting a different database would .dispose() of the list of buttons and create new ones with different values. Will the garbage collector pick them up? – GapWim Oct 30 '15 at 13:07
  • @GapWim GetNewClosure do the same thing: create new module and capture local scope variables to it. So, you should not have performance difference compared to it. And yes, garbage collector will collect modules, when you lose all reference to it: $Module=New-Module {};$WeakReference=New-Object WeakReference $Module;$WeakReference.IsAlive;$Module=$null;[GC]::Collect();$WeakReference.IsAlive. – PetSerAl Oct 30 '15 at 13:25
  • @GapWim I add a way to solve this without creating a module for each button. – PetSerAl Oct 30 '15 at 13:47
  • GetNewClosure do the same thing ah, I didn't know that … and it’s good to know, thanks. For your latest edit, that looks sleek! And it has the advantage that I can still keep the code for the GUI separated from the functional code. I’m presuming that (with the absence of New-Module or GetNewClosure) it doesn’t create a new module for each button in the same memory intensive way or am I seeing this wrong? – GapWim Oct 30 '15 at 14:07
  • @GapWim Yes it does not create new module, for every button, as it does not need to capture value of any local variable for specific button. Any button specific data stored in button itself. – PetSerAl Oct 30 '15 at 14:17
0

I pasted your code into a file buttons.ps1, and ran the file with powershell -File buttons.ps1. I got a new form window with all of the buttons, as expected. I also tried opening the file via "open with powershell" and that worked fine too.

I don't have a context menu item for "run with powershell" - but apparently that context entry is executing your script in a different way than the two above. Can you try executing your script from the windows command line?

  • Now this is interesting! When I run it in ISE, the script works. When run with it with the context menu, the script fails (so far the same as I’ve described in my OP). Now when launching the script from a command prompt with powershell -File buttons.ps1 it works just fine again (just like you tested). But when I first launch PowerShell and then open the file with .\buttons.ps1 it fails. It also fails when launching the script directly from a PS CLI … unless I launch it with the same command as the command prompt … then it works fine. Does this have something to do with profiles (or alike)? – GapWim Oct 30 '15 at 13:49
  • Likely a scope issue - something about the way the powershell code is executing leads to the function definition going out of scope. – user2831705 Nov 1 '15 at 10:54
  • Perhaps this is a knee-jerk reaction from me, but I’d say this is a major fault in PowerShell. I can –to some extent- understand a different behaviour in the ISE for developing reasons (Like PetSerAl originally explained) … but not when launching a script outside of the development environment. There should be no such difference between typing powershell.exe -File buttons.ps1 [Enter] in a dos box or typing powershell.exe [Enter] .\buttons.ps1 [Enter] in that same dos box. It’s like saying nslookup superuser.com [Enter] works, but nslookup [Enter] superuser.com [Enter] doesn’t. – GapWim Nov 2 '15 at 11:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.