如何在PowerShell中创建调用脚本函数的动态块?

huangapple go评论79阅读模式
英文:

How to create dynamic block in powershell, that calls script functions?

问题

我正在尝试生成一个动态用户界面。我无法动态添加OnClick事件。以下是一个示例

```powershell
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

首先,我尝试过$b1.Add_Click({Say-Start $name}),但会产生Hello XXXX。然后,我尝试了上面的代码$b1.Add_Click({Say-Hello $name}.GetNewClosure()),但我收到一个错误消息,提示找不到Say-Hello (Say-Hello : The term 'Say-Hello' is not recognized as the name of a cmdlet, function, script file...)

我覆盖$name的原因是因为我实际上想将按钮创建转换为一个函数,我将多次调用该函数,每次传入不同的$name参数。

有什么建议如何处理这个问题吗?

谢谢


<details>
<summary>英文:</summary>

I&#39;m trying to generate a dynamic UI. I haven&#39;t been able to add an OnClick event dynamically. Here&#39;s a sample

function Say-Hello
{
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String]$name
)

Write-Host &quot;Hello &quot; + $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&#39;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 &#39;Say-Hello&#39; is not recognized as the name of a cmdlet, function, script file...`)

The reason I&#39;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

</details>


# 答案1
**得分**: 1

以下是翻译好的部分:

看起来您想要使用脚本块来创建对您的$name变量状态的闭包,这意味着$name的值应该在创建闭包时通过.GetNewClosure() 固定,不受调用者范围中$name变量值后续更改的影响。

问题在于PowerShell使用动态模块来实现闭包,并且与外部调用者共享的唯一祖先范围是_全局_范围。

换句话说:.GetNewClosure()返回的动态模块不知道您的Say-Hello函数,因为它是在全局范围的范围中创建的,这是脚本和函数默认运行的地方。

  • 顺便说一下:如果您要从全局范围使用点源您的脚本,问题将消失,但这是不可取的,因为这样会污染全局范围,包括您的脚本中的所有变量、函数等定义。

  • 有选择地将函数定义为function global:Say-Hello { ... }是一个"不那么污染"的替代方案,但仍然不是最佳选择。

解决方案

在将创建闭包的脚本块的上下文中重新定义函数。

这是一个简化的、独立的示例:

& { # 在一个*子*范围中执行以下代码。

  $name = 'before' # 要固定的值。

  function Say-Hello { "Hello $name" } # 您的函数。

  # 从*字符串*创建脚本块的脚本块,其中您可以在动态模块的上下文中重新定义函数Say-Hello。
  $scriptBlockWithClosure = 
    [scriptblock]::Create("
      `${function:Say-Hello} = { ${function:Say-Hello} }
      Say-Hello `$name
    ").GetNewClosure()

  $name = 'after'

  # 调用脚本块,仍然将$name的值作为'before'
  & $scriptBlockWithClosure # -> 'Hello before'
}

${function:Say-Hello}是_命名空间变量表示法_的实例 - 有关通用背景信息,请参阅此答案

在获取诸如${function:Say-Hello}之类的表达式时,将返回目标函数的_主体_,作为[scriptblock]实例。

在为${function:Say-Hello}分配值时,将_定义_目标函数;分配值可以是_脚本块_或包含函数源代码的_字符串_(不包含{ ... }封装)

  • 在上面的代码中,使用可展开的(双引号括起的)字符串(即_字符串插值_)来嵌入${function:Say-Hello}返回的脚本块的字符串化源代码,该字符串传递给[scriptblock]::Create()

  • 通过将${function:Say-Hello}引用括在{ ... }中,${function:Say-Hello}的字符串化脚本块 - 该脚本块在不包含{ ... }封装的情况下进行字符串化 - 成为构造脚本块的源代码中的脚本块_文字_。


<details>
<summary>英文:</summary>

&lt;!-- language-all: sh --&gt;

It sounds like you want to use a [script block](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Script_Blocks) to create a *closure* over the state of your `$name` variable, meaning that the  value of `$name` should be _locked in_ at the time of creating the closure with [`.GetNewClosure()`](https://learn.microsoft.com/en-US/dotnet/api/System.Management.Automation.ScriptBlock.GetNewClosure), without  being affected by later changes to the value of the `$name` variable in the caller&#39;s scope.

The problem is that PowerShell uses a [*dynamic module*](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/new-module) to implement the closure, and - like *all* [modules](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Modules) - the only  ancestral [scope](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Scopes) 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 your `Say-Hello` function, 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](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Operators#dot-sourcing-operator-) 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 &quot;less polluting&quot; alternative, but still suboptimal.

----

**Solution**:

*Redefine* the function in the context of the script block for which the closure will be created.

Here&#39;s a simplified, stand-alone example:

& { # Execute the following code in a child scope.

$name = 'before' # The value to lock in.

function Say-Hello { "Hello $name" } # Your function.

Create a script block from a string inside of which you can redefine

function Say-Hello in the context of the dynamic module.

$scriptBlockWithClosure =
[scriptblock]::Create("
${function:Say-Hello} = { ${function:Say-Hello} }
Say-Hello
$name
").GetNewClosure()

$name = 'after'

Call the script block, which still has 'before' as the value of $name

& $scriptBlockWithClosure # -> 'Hello before'
}


* `${function:Say-Hello}` is an instance of _namespace variable notation_ - see [this answer](https://stackoverflow.com/a/55036515/45375) for general background information.

* On _getting_ an expression such as `${function:Say-Hello}`, the targeted function&#39;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&#39;s source code (without enclosing it in `{ ... }`)
  * In the above code, an [expandable (double-quoted) string (`&quot;...&quot;`)](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_Quoting_Rules#double-quoted-strings), 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()`](https://learn.microsoft.com/en-US/dotnet/api/System.Management.Automation.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.


</details>



# 答案2
**得分**: 0

以下是您提供的内容的翻译:

根据@mklement0的答案和评论,我编写了一个小示例来演示问题和解决方案。以下代码显示了以下选项:

1. 按钮“A” - 一个简单的块,使用内置(全局)cmdlet与一个“constant”字符串

2. 按钮“B” - 从参数动态生成块。这显示了问题 - “B”保存在传递给`AddButtonAutoBlock`的变量中,当按钮被按下时它不存在。打印的消息是空的。

3. 按钮“C” - 从块生成一个闭包,就像“B”一样。但是,“C”被复制,但全局范围中未知函数`Show-Message`,所以它会出错。

4. 按钮“D” - 污染全局范围,添加一个全局函数。这克服了“C”中的问题,并且有效。

5. 按钮“E” - 为了避免填充全局范围,使用一个回调函数。回调函数在内部将调用分派到正确的本地函数。

6. 按钮“F” - 使用全局回调,调用本地函数。这次调用更加通用。调用被定向回到实际保存按钮的同一对象中。

评论

- “E”具有一个if-else结构,需要扩展以适应每个新的回调,但“F”对所有回调使用相同的代码。但是,“E”在参数类型方面更加通用。`Call-LocalObjectCallbackString`正在调用`"$callback('$arg0')"`,假设`$arg0`是一个字符串。
- 是否有一种方法可以结合两者 - 具有通用参数列表的通用回调?我尝试传递一个列表,但问题在于`GetNewClosure`将数据转换为(似乎是)原始字符串。也许一些打包和解包操作可以在这里有所帮助。
- 这里显示的单例很简单,但有一个很好的,更正式的单例[这里][1]

```powershell
function Show-Message
{
    Param([String]$message)
	
    Write-Host "Message: $message"
}

function Show-Message-Beautify
{
    Param([String]$message)
	
    Write-Host "Message: <<<$message>>>"
}

function global:Show-Message-Global
{
    Param([String]$message)
	
    Show-Message $message
}

function global:Show-Message-Global-Callback
{
    Param($callback, $arg0)
	
    if ($callback -eq "Show-Message")
    {
        Show-Message $arg0
    }
    elseif  ($callback -eq "Show-Message-Beautify")
    {
        Show-Message-Beautify $arg0
    }
    else
    {
        # throw exception
    }
}

function global:Call-LocalObjectCallbackString
{
    Param($callback, $arg0)
	
    Invoke-Expression -Command "$callback('$arg0')"
}

class MainForm
{
    static [MainForm]$mainSingleton = $null
	
    static [MainForm] Instance()
    {
        return [MainForm]::mainSingleton
    }
	
    static ShowMainForm()
    {
        $null = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
		
        $main = [MainForm]::new()
        $main.AddButtonBlock("A", {Write-Host "A"})
        $main.AddButtonAutoBlock("B")
        $main.AddButtonAutoBlockClosure("C")
        $main.AddButtonAutoBlockGlobalClosure("D")
        $main.AddButtonAutoBlockGlobalClosureCallback("E")
        $main.AddButtonAutoBlockGlobalClosureCallbackObject("F")
		
        $main.form.ShowDialog() | Out-Null
    }
	
    # non statics

    $form
    [int] $nextButtonOffsetY

    MainForm()
    {
        $this.form = New-Object System.Windows.Forms.Form
        $this.form.Text = "test"
        $this.form.Size = New-Object System.Drawing.Size(200,400)
        $this.nextButtonOffsetY = 20
		
        [MainForm]::mainSingleton = $this
    }

    [object] AddButton($name)
    {
        $b = New-Object System.Windows.Forms.Button
        $b.Location = New-Object System.Drawing.Point(20, $this.nextButtonOffsetY)
        $b.Size = New-Object System.Drawing.Size(160,30)
        $b.Text = $name
		
        $this.nextButtonOffsetY += 40
        $this.form.Controls.Add($b)
		
        return $b
    }

    AddButtonBlock($name, $block)
    {
        $b = $this.AddButton($name)
        $b.Add_Click($block)
    }

    AddButtonAutoBlock($name)
    {
        $b = $this.AddButton($name)
        $b.Add_Click({Show-Message $name})
    }
	
    AddButtonAutoBlockClosure($name)
    {
        $b = $this.AddButton($name)
        $b.Add_Click({Show-Message $name}.GetNewClosure())
    }

    AddButtonAutoBlockGlobalClosure($name)
    {
        $b = $this.AddButton($name)
        $b.Add_Click({Show-Message-Global $name}.GetNewClosure())
    }
	
    AddButtonAutoBlockGlobalClosureCallback($name)
    {
        $b = $this.AddButton($name)
        $b.Add_Click({Show-Message-Global-Callback "Show-Message" $name}.GetNewClosure())

        $b = $this.AddButton("Beautify-$name")
        $b.Add_Click({Show-Message-Global-Callback "Show-Message-Beautify" $name}.GetNewClosure())
    }
	
    Callback ($message)
    {
        Write-Host "Callback: $message"
    }
	
    AddButtonAutoBlockGlobalClosureCallbackObject($name)
    {
        $b = $this.AddButton($name)
        $b.Add_Click({Call-LocalObjectCallbackString "[MainForm]::Instance().Callback" $name}.GetNewClosure())
    }
}

[MainForm]::ShowMainForm()
英文:

Following @mklement0's answer and comment I wrote a small sample to demonstrate the issue and a solution. The code below shows the next options

  1. Button "A" - a simple block, using a built-in (global) cmdlet with a 'constant' string

  2. Button "B" - generating block dynamically from parameters. This shows the problem - "B" is kept in a variable passed to AddButtonAutoBlock and it does not exist when the button is pressed. The message prints is empty.

  3. Button "C" - a closure is generated from the block as in "B". However, "C" is copied, but the function Show-Message is unknown from the global scope, so it get's an error

  4. Button "D" - polluting the global scope with a global function. This overcomes the problem in "C" and works

  5. Button "E" - to avoid filling the global scope with this script's functions a single callback is used. The callback internally dispatches the call to the right local function

  6. Button "F" - a global callback is used, calling a local function. This time the call is a bit more generic. The call is directed back into the same object that actually holds the button.

Comments

  • "E" has an if-else structure that needs to be extended for each new callback, but "F" is using the same code for all callbacks. However, "E" is more generic regarding parameter types. Call-LocalObjectCallbackString is invoking &quot;$callback(&#39;$arg0&#39;)&quot; assuming $arg0 is a string
  • Is there a way to combine both - have a generic call back with generic parameter list? I tried passing a list, but the issue is that GetNewClosure converts the data to (what seems like) a raw string. Perhaps some pack and unpack operations can help here.
  • The singleton show here is trivial, but there's a nice, more formal one here
function Show-Message
{
    Param([String]$message)
	
	Write-Host &quot;Message: $message&quot;
}

function Show-Message-Beautify
{
    Param([String]$message)
	
	Write-Host &quot;Message: &lt;&lt;&lt;$message&gt;&gt;&gt;&quot;
}

function global:Show-Message-Global
{
    Param([String]$message)
	
	Show-Message $message
}

function global:Show-Message-Global-Callback
{
    Param($callback, $arg0)
	
	if ($callback -eq &quot;Show-Message&quot;)
	{
		Show-Message $arg0
	}
	elseif  ($callback -eq &quot;Show-Message-Beautify&quot;)
	{
		Show-Message-Beautify $arg0
	}
	else
	{
		# throw exception
	}
}

function global:Call-LocalObjectCallbackString
{
    Param($callback, $arg0)
	
	Invoke-Expression -Command &quot;$callback(&#39;$arg0&#39;)&quot;
}

class MainForm
{
	static [MainForm]$mainSingletone = $null
	
	static [MainForm] Instance()
	{
        return [MainForm]::mainSingletone
    }
	
	static ShowMainForm()
	{
		$null = [System.Reflection.Assembly]::LoadWithPartialName(&quot;System.Windows.Forms&quot;)
		
		$main = [MainForm]::new()
		$main.AddButtonBlock(&quot;A&quot;, {Write-Host &quot;A&quot;})
		$main.AddButtonAutoBlock(&quot;B&quot;)
		$main.AddButtonAutoBlockClosure(&quot;C&quot;)
		$main.AddButtonAutoBlockGlobalClosure(&quot;D&quot;)
		$main.AddButtonAutoBlockGlobalClosureCallback(&quot;E&quot;)
		$main.AddButtonAutoBlockGlobalClosureCallbackObject(&quot;F&quot;)
		
		$main.form.ShowDialog() | Out-Null
	}
	
	# non statics

    $form
    [int] $nextButtonOffsetY

    MainForm()
	{
        $this.form = New-Object System.Windows.Forms.Form
		$this.form.Text = &quot;test&quot;
		$this.form.Size = New-Object System.Drawing.Size(200,400)
		$this.nextButtonOffsetY = 20
		
		[MainForm]::mainSingletone = $this
    }

    [object] AddButton($name)
	{
		$b = New-Object System.Windows.Forms.Button
		$b.Location = New-Object System.Drawing.Point(20, $this.nextButtonOffsetY)
		$b.Size = New-Object System.Drawing.Size(160,30)
		$b.Text = $name
		
		$this.nextButtonOffsetY += 40
		$this.form.Controls.Add($b)
		
		return $b
    }

    AddButtonBlock($name, $block)
	{
		$b = $this.AddButton($name)
		$b.Add_Click($block)
    }

    AddButtonAutoBlock($name)
	{
		$b = $this.AddButton($name)
		$b.Add_Click({Show-Message $name})
    }
	
    AddButtonAutoBlockClosure($name)
	{
		$b = $this.AddButton($name)
		$b.Add_Click({Show-Message $name}.GetNewClosure())
    }

    AddButtonAutoBlockGlobalClosure($name)
	{
		$b = $this.AddButton($name)
		$b.Add_Click({Show-Message-Global $name}.GetNewClosure())
    }
	
    AddButtonAutoBlockGlobalClosureCallback($name)
	{
		$b = $this.AddButton($name)
		$b.Add_Click({Show-Message-Global-Callback &quot;Show-Message&quot; $name}.GetNewClosure())

		$b = $this.AddButton(&quot;Beautify-$name&quot;)
		$b.Add_Click({Show-Message-Global-Callback &quot;Show-Message-Beautify&quot; $name}.GetNewClosure())
    }
	
	Callback ($message)
	{
		Write-Host &quot;Callback: $message&quot;
	}
	
    AddButtonAutoBlockGlobalClosureCallbackObject($name)
	{
		$b = $this.AddButton($name)
		$b.Add_Click({Call-LocalObjectCallbackString &quot;[MainForm]::Instance().Callback&quot; $name}.GetNewClosure())
    }
}

[MainForm]::ShowMainForm()

huangapple
  • 本文由 发表于 2023年7月24日 15:45:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76752344.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定