【问题标题】:How to add an event Action handler in PowerShell如何在 PowerShell 中添加事件操作处理程序
【发布时间】:2020-10-02 21:51:34
【问题描述】:

Terminal.Gui (gui.cs) 提供了一个Button 类,其中一个Clicked 事件定义为:

        public event Action Clicked;

我正在尝试在 PowerShell 中为 Terminal.Gui 编写示例应用程序,并且正在努力连接事件处理程序。

Add-Type -AssemblyName Terminal.Gui
[Terminal.Gui.Application]::Init() 
$win = New-Object Terminal.Gui.Window
$win.Title = "Hello World"
$btn = New-Object Terminal.Gui.Button
$btn.X = [Terminal.Gui.Pos]::Center()
$btn.Y = [Terminal.Gui.Pos]::Center()
$btn.Text= "Press me"

# Here lies dragons
[Action]$btn.Clicked = {
    [Terminal.Gui.Application]::RequestStop() 
}

$win.Add($btn)

[Terminal.Gui.Application]::Top.Add($win)
[Terminal.Gui.Application]::Run()  

上面示例中的Clicked = 赋值返回错误:

InvalidOperation: The property 'Clicked' cannot be found on this object. Verify that the property exists and can be set.

但智能感知自动完成 Clicked 对我来说......所以我猜这是一个类型问题?

我在 [Action] 上找不到任何 PowerShell 文档,而且我发现的其他示例也没有让我感到高兴。

如何在 PowerShell 中为基于 Action 的 dotnet 事件定义事件处理程序?

【问题讨论】:

    标签: .net powershell event-handling terminal.gui


    【解决方案1】:

    C# 代码将添加一个 lambda:

    btn.Clicked += ...
    

    所以在 PowerShell 中,需要显式调用 Add_Clicked() 方法:

    $btn.Add_Clicked({
        param($sender,$e)
        [Terminal.Gui.Application]::RequestStop()
    })
    

    参数与方法签名相匹配,尽管在此示例中未使用。

    【讨论】:

    • 很好,但请注意您显示的标准签名(更具体地说,param([object] $sender, [EventArgs] $eventArgs),其中一个类 派生自 [EventArgs] 按照惯例用于传递实际arguments) 碰巧在这里不适用,因为所讨论的事件是以非标准方式定义的:它被声明为采用Action 委托,这是无参数。术语狡辩:事件处理程序是委托,在 C# 中,常规方法和 lambda 表达式(匿名方法)都可以这样。
    【解决方案2】:

    Steve Lee's helpful answer 提供关键指针;让我补充一下背景信息

    PowerShell 提供了两种基本的事件订阅机制:

    • (a) .NET-native,如 Steve 的回答中所示,您将 script block ({ ... }) 作为 委托 附加到对象的 @ 987654339@ 事件通过.add_<Name>() 实例方法(委托是一段用户提供的回调代码,在事件触发时被调用) - 请参阅下一节。

    • (b) PowerShell 介导,使用 Register-ObjectEvent 和相关 cmdlet:

      • 类似于 (a) 的基于回调的方法可通过将脚本块传递给 -Action 参数来获得。
      • 或者,可以通过Get-Event cmdlet 按需检索排队的事件。

    方法 (b) 的回调方法仅在 PowerShell 控制前台线程时及时起作用,而 不是 这里的情况,因为[Terminal.Gui.Application]::Run() 调用阻止它。 因此,必须使用方法(a)。


    回复(a):

    C# 以运算符+=-= 的形式提供语法糖,用于附加和分离事件处理程序委托,这看起来assignments,但实际上被翻译成add_<Event>()remove_<Event>()方法调用

    可以看到这些方法名如下,以[powerShell]类型为例:

    PS> [powershell].GetEvents() | select Name, *Method, EventHandlerType
    
    
    Name             : InvocationStateChanged
    AddMethod        : Void add_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
    RemoveMethod     : Void remove_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
    RaiseMethod      : 
    EventHandlerType : System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs]
    
    

    PowerShell 没有提供这样的语法糖来附加/删除事件处理程序,因此必须直接调用这些方法。

    不幸的是,Get-Member 和 tab-completion 都不知道这些方法,而相反,原始事件 names 令人困惑地do 得到 tab-completed,即使你不能直接对他们采取行动。

    Github suggestion #12926 旨在解决这两个问题。

    用于事件定义的约定

    上面的EventHandlerType 属性显示了事件处理程序委托的类型名称,在这种情况下,它正确地遵守了使用基于泛型类型System.EventHandler<TEventArgs> 的委托的约定,其签名是:

    public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
    

    TEventArgs 表示包含事件特定信息的实例的类型。 另一个约定是这样的事件参数类型派生自 System.EventArgs 类,而手头的类型 PSInvocationStateChangedEventArgs 就是这样。

    提供no事件特定信息的事件按照约定使用非通用System.EventHandler委托:

    public delegate void EventHandler(object? sender, EventArgs e);
    

    大概是因为这个委托在历史上被用于所有委托,即使是那些带有事件参数的委托——在generics出现在.NET之前2 - EventArgs 参数仍然存在,约定是传递 EventArgs.Empty 而不是 null 来表示没有参数。
    类似地,长期建立的框架类型使用其特定的事件参数类型定义非泛型 custom 委托,例如System.Windows.Forms.KeyPressEventHandler.

    CLR 没有强制执行这些约定,但是,正如所讨论的事件被定义为public event Action Clicked; 所证明的那样,它使用无参数 委托作为事件处理程序。

    通常建议遵守约定,以免违背用户的期望,尽管这样做有时不太方便。


    PowerShell 在将脚本块 ({ ... }) 用作委托时非常灵活,尤其是 强制执行特定的参数签名 通过param(...):

    脚本块被接受,不管它是否声明了任何、太多或太少的参数,尽管那些绑定到脚本块参数的事件发起对象实际传递的参数必须是类型兼容的(假设脚本块的参数是显式类型的)。​​

    因此,史蒂夫的代码:

    $btn.Add_Clicked({
        param($sender, $e)
        [Terminal.Gui.Application]::RequestStop()
    })
    

    尽管参数声明无用,但仍然有效,因为没有参数被传递给脚本块,因为 System.Action 委托类型是无参数

    以下内容就足够了:

    $btn.Add_Clicked({
      [Terminal.Gui.Application]::RequestStop()
    })
    

    注意:即使不声明参数,您也可以通过automatic $this variable(在本例中与$btn 相同)引用事件发送者(触发事件的对象)。


    简化的示例代码

    1.0.0-pre.4 版本的注释:

    • 至少在 macOS 上,要让终端在退出应用程序后将终端恢复到可用状态,还需要以下附加操作:

      • [Terminal.Gui.Application]::Shutdown()。没有它,在同一个会话中重新调用应用程序将不起作用。
      • tput init。没有它,后来的命令行编辑就会中断(特别是向上和向下箭头)。
    • Terminal.Gui 类型对 PowerShell 不友好,原因有两个:

      • [View] 及其子类实现 IEnumerable 接口,这会导致 PowerShell 的默认输出格式尝试枚举,从而导致 no 输出。

        • 解决方法:$btn.psobject.Properties | select Name, Value, TypeNameOfValue
      • 什么是概念上的 text 属性不是作为[string] 类型实现的,而是作为[NStack.ustring] 实现的;虽然您可以透明地使用[string] 实例来分配此类属性,但显示它们再次执行枚举并呈现底层字符的代码点 个别

        • 解决方法:致电.ToString()
      • tig(OP)已提交 GitHub issue #951 以可能修复此行为。

    • 从 PowerShell 7.1 开始,没有与 NuGet 包的直接集成,因此将已安装包的程序集加载到 PowerShell 会话中非常麻烦 - 请参阅 this answer,其中展示了如何使用 .NET Core SDK 下载包并使其依赖项可用

      • 请注意,Add-Type -AssemblyName 仅适用于位于 当前 目录(与 脚本的 目录相反)或随 PowerShell 本身提供的程序集(PowerShell [Core ] v6+) / 在 GAC (Windows PowerShell) 中。

      • 鉴于目前在 PowerShell 中使用 NuGet 包非常繁琐,GitHub feature suggestion #6724 要求增强 Add-Type 以直接支持 NuGet 包。

    using namespace Terminal.Gui
    
    # Load the Terminal.Gui assembly and its dependencies (assumed to be in the
    # the same directory).
    # NOTE: `using assmembly <path>` seemingly only works with full, literal paths
    # as of PowerShell Core 7.1.0-preview.7.
    # The assumption here is that all relevant DLLs are stored in subfolder
    # assemblies/bin/Release/*/publish of the script directory, as shown in 
    #   https://stackoverflow.com/a/50004706/45375
    Add-Type -Path $PSScriptRoot/assemblies/bin/Release/*/publish/Terminal.Gui.dll
    
    # Initialize the "GUI".
    # Note: This must come before creating windows and controls.
    [Application]::Init()
    
    $win = [Window] @{
      Title = 'Hello World'
    }
    
    $btn = [Button] @{
      X = [Pos]::Center()
      Y = [Pos]::Center()
      Text = 'Quit'
    }
    $win.Add($btn)
    [Application]::Top.Add($win)
    
    # Attach an event handler to the button.
    # Note: Register-ObjectEvent -Action is NOT an option, because
    # the [Application]::Run() method used to display the window is blocking.
    $btn.add_Clicked({
      # Close the modal window.
      # This call is also necessary to stop printing garbage in response to mouse
      # movements later.
      [Application]::RequestStop()
    })
    
    # Show the window (takes over the whole screen). 
    # Note: This is a blocking call.
    [Application]::Run()
    
    # As of 1.0.0-pre.4, at least on macOS, the following two statements
    # are necessary on in order for the terminal to behave properly again.
    [Application]::Shutdown() # Clears the screen too; required for being able to rerun the application in the same session.
    tput init # Reset the terminal to make PSReadLine work properly again, notably up- and down-arrow.
    

    【讨论】:

      【解决方案3】:

      此更改未显示错误,但事件似乎正在触发。

      Register-ObjectEvent -InputObject $btn -EventName Clicked --Action {
              [Terminal.Gui.Application]::RequestStop() 
      }
      

      编辑:

      @Steve Lee 的解决方案就像一个魅力,但还需要在末尾添加[Terminal.Gui.Application]::Shutdown()。不需要param($sender,$e),因为它不是EventHandler,而是event Action。谢谢。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-09
        • 2013-08-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多