I'm trying to generate a dynamic UI. I haven't been able to add an OnClick event dynamically. Here's a sample
function Say-Hello
{
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$name
)
Write-Host "Hello " + $name
}
$name = "World"
$null = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$mainform = New-Object System.Windows.Forms.Form
$b1 = New-Object System.Windows.Forms.Button
$b1.Location = New-Object System.Drawing.Point(20, 20)
$b1.Size = New-Object System.Drawing.Size(80,30)
$b1.Text = "Start"
#$b1.Add_Click({Say-Hello $name})
$b1.Add_Click({Say-Hello $name}.GetNewClosure())
$mainform.Controls.Add($b1)
$name = "XXXX"
$mainform.ShowDialog() | Out-Null
First I've tried with $b1.Add_Click({Say-Start $name}) but that yields Hello XXXX. I then tried the above code as it is $b1.Add_Click({Say-Hello $name}.GetNewClosure()) and I got an error that Say-Hello is not found (Say-Hello : The term 'Say-Hello' is not recognized as the name of a cmdlet, function, script file...)
The reason I'm overriding the name, is because I actually want to turn the button creation to a function that I will call several ties, each time with a different $name parameter.
Any suggestions how to handle this?
thanks
It sounds like you want to use a script block to create a closure over the state of your
$namevariable, meaning that the value of$nameshould be locked in at the time of creating the closure with.GetNewClosure(), without being affected by later changes to the value of the$namevariable in the caller's scope.The problem is that PowerShell uses a dynamic module to implement the closure, and - like all modules - the only ancestral scope a dynamic module shares with an outside caller is the global scope.
In other words: the dynamic module returned by
.GetNewClosure()does not know about yourSay-Hellofunction, because it was created in a child scope of the global scope, which is where scripts and functions run by default.As an aside: If you were to dot-source your script from the global scope, the problem would go away, but that is undesirable, because you would then pollute the global scope with all the variable, function, ... definitions in your script.
Selectively defining your function as
function global:Say-Hello { ... }is a "less polluting" alternative, but still suboptimal.Solution:
Redefine the function in the context of the script block for which the closure will be created.
Here's a simplified, stand-alone example:
${function:Say-Hello}is an instance of namespace variable notation - see this answer for general background information.On getting an expression such as
${function:Say-Hello}, the targeted function's body is returned, as a[scriptblock]instance.On assigning to
${function:Say-Hello}, the targeted function is defined; the assignment value can either be a script block or a string containing the function's source code (without enclosing it in{ ... })In the above code, an expandable (double-quoted) string (
"..."), i.e. string interpolation is used to embed the stringified source code of the script block returned by${function:Say-Hello}in the string passed to[scriptblock]::Create()By enclosing the
${function:Say-Hello}reference in{ ... }, the stringified script block - which stringifies without the{ ... }enclosure - becomes a script block literal in the source code from which the script block is constructed.