2

I am working on a PowerShell script that creates a GUI, using classes from Windows.Forms. The script runs fine in the PowerShell ISE, but when I select "Run with PowerShell" from the context menu in File Explorer, it fails and doesn't find the functions defined in the script.

Digging a bit further into the problem, I found that the "Run with PowerShell" invocation more or less executes a command line like

powershell.exe -Command "script.ps1"

while running the script within the ISE is equivalent to

powershell.exe -File "script.ps1"

Running these two commands from a cmd window exactly reproduce what I observed: The -File version works well and the -Command version fails.

I am including a stripped-down version of the script. It creates a GUI with the buttons "Save configuration" and "Exit". The "Exit" button does what you expect, and "Save configuration" calls the functions Get-Configuration-Data-from-GUI and Save-Configuration (which both do nothing). When you launch the script in the ISE or via the -File parameter, all is well, but when you try "Run with PowerShell" or the -Command parameter, you see these errors when you click "Save configuration":

Get-Configuration-Data-from-GUI : The term 'Get-Configuration-Data-from-GUI' 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:\Projects\Internal\Tools\VersionInventoryTool\trunk\source\VersionInventory.ps1:54 char:9
...
Save-Configuration : The term 'Save-Configuration' 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:\Projects\Internal\Tools\VersionInventoryTool\trunk\source\VersionInventory.ps1:55 char:9
...

This is what's left of my script after removing all unnecessary parts; the error is reported from the script block that is the argument to $save_button.Add_Click:

using namespace System.Windows.Forms
using namespace System.Drawing

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

function Save-Configuration()
{
}

function Get-Configuration-Data-from-GUI($gui)
{
}

function New-Element
{
    param(
        [parameter()] [string] $class,
        [parameter()] [int] $x,
        [parameter()] [int] $y,
        [parameter()] [int] $width,
        [parameter()] [int] $height,
        [parameter()] [string] $text,
        [parameter(Mandatory = $false)] [Form] $parent
    )

    $ret = New-Object $class
    if ($width -gt 0)
    {
        $ret.Width = $width
    }
    if ($height -gt 0)
    {
        $ret.Height = $height
    }
    $ret.Location  = New-Object System.Drawing.Point($x, $y)
    $ret.Text = $text
    if ($parent -ne $null)
    {
        $parent.Controls.Add($ret)
    }
    return $ret
}

function Create-GUI()
{
    $main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
    $border = 10
    $width = $main_form.ClientRectangle.Width - 2 * $border

    $save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
    $save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
    $save_button.Add_Click({
        Get-Configuration-Data-from-GUI $main_form
        Save-Configuration
    }.GetNewClosure())

    $exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
    $exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
    $exit_button.DialogResult = [DialogResult]::OK

    return $main_form
}

$gui = Create-Gui
$gui.ShowDialog()

Hoping for insights...

Greetings Hans

2

1 Answer 1

1

After Create-GUI() is done, the scope for {...} in button.Add_Click({...$main_form...}.GetNewClosure()) is kinda left dangling which is why it can't get back to $main_form; you've over compensated for this by adding the .GetNewClosure() which more or less creates a whole new scope and copies $main_form into it. However this new scope is tenuously attached to the scope that Create-GUI() lives in, and like you've seen depending on how Create-GUI() is sourced into the runspace (& aka -Command vs . aka -File), then {}.GetNewClosure() may or may not have affinity to Create-GUI()'s parent runspace.

To get around this I've dropped the .GetNewClosure() to keep {...} in the runspace it looks like it should live in, and moved the .ShowDialogue() into Create-GUI() so that $main_form doesn't go out of scope.

using namespace System.Windows.Forms
using namespace System.Drawing

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

function Save-Configuration{
   param($cfg)
   Write-Host ('Mocked: Save-Configuration({0})' -f $cfg) -ForegroundColor Magenta
}

function Get-Configuration-Data-from-GUI{
   param($gui)
   Write-Host ('Mocked: Get-Configuration-Data-from-GUI({0})' -f $gui.Text) -ForegroundColor Magenta
   return $gui.Text
}

function New-Element{
   param(
      [parameter()] [string] $class,
      [parameter()] [int] $x,
      [parameter()] [int] $y,
      [parameter()] [int] $width,
      [parameter()] [int] $height,
      [parameter()] [string] $text,
      [parameter(Mandatory = $false)] [Form] $parent
   )

   $ret = New-Object $class
   if($width -gt 0){
      $ret.Width = $width
   }
   if($height -gt 0){
      $ret.Height = $height
   }
   $ret.Location  = New-Object System.Drawing.Point($x, $y)
   $ret.Text = $text
   if($parent -ne $null){
      $parent.Controls.Add($ret)
   }
   return $ret
}

function Show-GUI(){
   $gui_data = [ordered]@{
      cfg = ''
      res = $null
   }
   
   $main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
   $border = 10
   $width = $main_form.ClientRectangle.Width - 2 * $border

   $save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
   $save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
   $save_button.Add_Click({
      $gui_data.cfg = Get-Configuration-Data-from-GUI $main_form
      Save-Configuration $gui_data.cfg
   })

   $exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
   $exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
   $exit_button.DialogResult = [DialogResult]::OK

   $gui_data.res = $main_form.ShowDialog() 
   return $gui_data
}

Show-GUI
pause

You could even take it a step further by either eliminating Save-Configuration and Get-Configuration-Data-from-GUI, or moving them into the same scope as button.Add_Click({...}). Which would then allow these two functions to access the form, controls, and data directly instead of needing to pass arguments.

using namespace System.Windows.Forms
using namespace System.Drawing

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

function Save-Configuration{
   Write-Host ('Mocked: Save-Configuration({0})' -f $gui_data.cfg) -ForegroundColor Magenta
}

function Get-Configuration-Data-from-GUI{
   Write-Host ('Mocked: Get-Configuration-Data-from-GUI({0})' -f $main_form.Text) -ForegroundColor Magenta
   $gui_data.cfg = $main_form.Text
}

function New-Element{
   param(
      [parameter()] [string] $class,
      [parameter()] [int] $x,
      [parameter()] [int] $y,
      [parameter()] [int] $width,
      [parameter()] [int] $height,
      [parameter()] [string] $text,
      [parameter(Mandatory = $false)] [Form] $parent
   )

   $ret = New-Object $class
   if($width -gt 0){
      $ret.Width = $width
   }
   if($height -gt 0){
      $ret.Height = $height
   }
   $ret.Location  = New-Object System.Drawing.Point($x, $y)
   $ret.Text = $text
   if($parent -ne $null){
      $parent.Controls.Add($ret)
   }
   return $ret
}

$gui_data = [ordered]@{
   cfg = ''
   res = $null
}

$main_form = New-Element Form -x 100 -y 100 -width 600 -height 500 -text "My tool"
$border = 10
$width = $main_form.ClientRectangle.Width - 2 * $border

$save_button = New-Element Button -text "Save configuration" -x $border -width 120 -parent $main_form
$save_button.Top = $main_form.ClientRectangle.Bottom - $border - $save_button.Height
$save_button.Add_Click({
   Get-Configuration-Data-from-GUI
   Save-Configuration
})

$exit_button = New-Element Button -text Exit -y $save_button.Top -parent $main_form
$exit_button.Left = $main_form.ClientRectangle.Right - $border - $exit_button.Width
$exit_button.DialogResult = [DialogResult]::OK

$gui_data.res = $main_form.ShowDialog() 
return $gui_data
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.