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

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

提问人:tamir 提问时间:7/24/2023 最后编辑:mklement0tamir 更新时间:7/28/2023 访问量:77

问:

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

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

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

我之所以要覆盖这个名称,是因为我实际上想将按钮创建转换为一个函数,我将调用多个 ties,每次都使用不同的参数。$name

有什么建议如何处理吗?

谢谢

PowerShell 动态 范围 闭包 powershell-module

评论

2赞 mclayton 7/24/2023
澄清一下,您是在期待还是在控制台上?当我运行您的代码时,我看到打印到控制台(尽管您可能想使用 or 代替)Hello WorldHello XXXXHello + WorldWrite-Host ("Hello " + $name)Write-Host "Hello $name"
0赞 tamir 7/26/2023
奇怪,我得到了.我试图得到@mklement0答案来解释它Hello XXXXHello World
0赞 mklement0 7/26/2023
@mclayton的评论是指出一个语法问题:逐字打印,因为操作周围缺少封闭意味着传递了三个单独的参数。至于答案:很高兴听到它解释了你的意图,但它也能解决你的问题吗?Write-HostWrite-Host 'hi ' + 'there'hi + there(...)+

答:

1赞 mklement0 7/24/2023 #1

听起来您想使用脚本块在变量的状态上创建一个闭包,这意味着在使用 .GetNewClosure(),而不受调用方作用域中变量值的后续更改的影响。$name$name$name

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

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

  • 顺便说一句:如果你要从全局作用域点你的脚本,问题就会消失,但这是不可取的,因为这样你就会用所有的变量、函数、......脚本中的定义。

  • 有选择地将您的功能定义为“污染较少”的替代方案,但仍然不是最佳选择。function global:Say-Hello { ... }


解决方案

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

下面是一个简化的独立示例:

& { # 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}命名空间变量表示法的实例 - 有关一般背景信息,请参阅此答案

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

  • 赋值时,定义目标函数;赋值可以是脚本块,也可以是包含函数源代码的字符串(不将其包含在${function:Say-Hello}{ ... })

评论

0赞 tamir 7/26/2023
感谢您的详细解释。由于错误消息,我确实怀疑了一些范围问题,但我不知道如何处理它。现在,我实施了你提出的一个建议,这个建议正在污染全球范围。主要是因为我想保持简单。我的下一个问题是,如果调用其他函数会怎样。因此,我最终实现了一个函数,该函数通过名称调用任何本地函数,因此我将污染降至最低Say-Helloglobal:Call-LocalFunction
0赞 mklement0 7/26/2023
感谢您的反馈,@tamir。我鼓励你根据你的评论写出你自己的答案。
0赞 tamir 7/28/2023 #2

根据 @mklement0 的回答和评论,我写了一个小样本来演示问题和解决方案。下面的代码显示了接下来的选项

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

  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
  • 这里的单身节目是微不足道的,但这里有一个很好的、更正式的节目
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]$mainSingletone = $null
    
    static [MainForm] Instance()
    {
        return [MainForm]::mainSingletone
    }
    
    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]::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 "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()