如何在 PowerShell 中添加事件操作处理程序
How to add an event Action handler in PowerShell
Terminal.Gui (gui.cs) 提供 Button
class 和 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 =
赋值returns出错:
InvalidOperation: The property 'Clicked' cannot be found on this object. Verify that the property exists and can be set.
但是 intellisense 会自动为我完成 Clicked
...所以我猜这是一个类型问题?
我在 [Action]
上找不到任何 PowerShell 文档,而且我发现的其他示例也没有给我带来任何乐趣。
如何在 PowerShell 中为基于 Action
的 dotnet 事件定义事件处理程序?
此更改未显示错误,但事件似乎未触发。
Register-ObjectEvent -InputObject $btn -EventName Clicked --Action {
[Terminal.Gui.Application]::RequestStop()
}
编辑:
@Steve Lee 的解决方案非常有效,但还需要在末尾添加 [Terminal.Gui.Application]::Shutdown()
。不需要 param($sender,$e)
,因为它不是 EventHandler
,而是 event Action
。谢谢。
C# 代码将添加一个 lambda:
btn.Clicked += ...
因此在 PowerShell 中,您需要显式调用 Add_Clicked() 方法:
$btn.Add_Clicked({
param($sender,$e)
[Terminal.Gui.Application]::RequestStop()
})
参数与方法签名匹配,但未在此示例中使用。
提供关键指针;让我补充一下 背景信息:
PowerShell 提供两种基本的 event-subscription 机制:
(a) .NET-native,如史蒂夫的回答所示,您在其中附上 script block ({ ... }
) 通过 .add_<Name>()
实例方法作为对象的 <Name>
事件的 委托 (委托是要调用的一段 user-supplied 回调代码当偶数触发时) - 请参阅下一节。
(b) PowerShell-mediated,使用 Register-ObjectEvent
和相关的 cmdlet:
- 一种类似于 (a) 的 callback-based 方法,可通过将脚本块传递给
-Action
参数来实现。
- 或者,可以通过
Get-Event
cmdlet 按需检索排队的事件。
方法(b)的回调方法只在PowerShell控制前台线程时及时起作用,不是[=222] =] 这里的情况,因为 [Terminal.Gui.Application]::Run()
调用 块 它。
因此,必须使用方法(a)。
回复(一):
C# 以运算符 +=
和 -=
的形式提供 语法糖 用于附加和分离 event-handler 个委托 , 看起来 像 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 为attaching/removing事件处理程序提供了没有这样的语法糖,因此必须直接调用这些方法。
不幸的是,Get-Member
和 tab-completion 都不知道这些方法,而相反,原始事件 names 令人困惑地 do 得到 tab-completed,即使你不能直接对它们采取行动。
Github suggestion #12926旨在解决这两个问题。
用于事件定义的约定:
上面的 EventHandlerType
属性 显示了 event-handler 委托的类型名称,在这种情况下它正确地遵守了 约定 使用基于泛型类型 System.EventHandler<TEventArgs>
的委托,其签名为:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
TEventArgs
表示包含event-specific信息的实例类型。
另一个约定是这样的event-arguments类型是从System.EventArgs
class, which the type at hand, PSInvocationStateChangedEventArgs
派生出来的,是
提供无event-specific信息的事件按照约定使用non-genericSystem.EventHandler
委托:
public delegate void EventHandler(object? sender, EventArgs e);
大概是因为这个代表在历史上曾用于 所有 代表,甚至是那些 with 事件参数 - 在 之前generics 在 .NET 2 中出现 - EventArgs
参数仍然存在,约定是传递 EventArgs.Empty
而不是 null
来表示没有参数。
类似地,long-established 框架类型定义 non-generic custom 委托及其特定的 event-arguments 类型,例如System.Windows.Forms.KeyPressEventHandler
.
None 这些约定由 CLR 强制实施,但是,所定义的相关事件证明了这一点作为 public event Action Clicked;
,它使用 无参数 委托作为事件处理程序。
通常建议遵守约定,以免违背用户期望,尽管这样做有时不太方便。
PowerShell 在使用脚本块 ({ ... }
) 作为委托时非常灵活,值得注意的是 not 强制执行特定的 参数签名 通过param(...)
:
脚本块被接受,无论它是否声明了任何参数,过多或过少,尽管 event-originating 对象实际传递的那些参数确实绑定到 script-block 参数必须是 type-compatible(假设脚本块的参数是明确键入的)。
因此,Steve 的代码:
$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 上,为了使终端 return 终端在退出应用程序后进入可用状态,以下附加操作是需要:
[Terminal.Gui.Application]::Shutdown()
。没有它,在同一个会话中重新调用应用程序将不起作用。
tput init
。没有它,后来的 command-line 编辑失败了(值得注意的是,向上和 down-arrow)。
[=177=Terminal.Gui
类型不是 PowerShell-friendly,原因有二:
[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
#
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.
Terminal.Gui (gui.cs) 提供 Button
class 和 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 =
赋值returns出错:
InvalidOperation: The property 'Clicked' cannot be found on this object. Verify that the property exists and can be set.
但是 intellisense 会自动为我完成 Clicked
...所以我猜这是一个类型问题?
我在 [Action]
上找不到任何 PowerShell 文档,而且我发现的其他示例也没有给我带来任何乐趣。
如何在 PowerShell 中为基于 Action
的 dotnet 事件定义事件处理程序?
此更改未显示错误,但事件似乎未触发。
Register-ObjectEvent -InputObject $btn -EventName Clicked --Action {
[Terminal.Gui.Application]::RequestStop()
}
编辑:
@Steve Lee 的解决方案非常有效,但还需要在末尾添加 [Terminal.Gui.Application]::Shutdown()
。不需要 param($sender,$e)
,因为它不是 EventHandler
,而是 event Action
。谢谢。
C# 代码将添加一个 lambda:
btn.Clicked += ...
因此在 PowerShell 中,您需要显式调用 Add_Clicked() 方法:
$btn.Add_Clicked({
param($sender,$e)
[Terminal.Gui.Application]::RequestStop()
})
参数与方法签名匹配,但未在此示例中使用。
PowerShell 提供两种基本的 event-subscription 机制:
(a) .NET-native,如史蒂夫的回答所示,您在其中附上 script block (
{ ... }
) 通过.add_<Name>()
实例方法作为对象的<Name>
事件的 委托 (委托是要调用的一段 user-supplied 回调代码当偶数触发时) - 请参阅下一节。(b) PowerShell-mediated,使用
Register-ObjectEvent
和相关的 cmdlet:- 一种类似于 (a) 的 callback-based 方法,可通过将脚本块传递给
-Action
参数来实现。 - 或者,可以通过
Get-Event
cmdlet 按需检索排队的事件。
- 一种类似于 (a) 的 callback-based 方法,可通过将脚本块传递给
方法(b)的回调方法只在PowerShell控制前台线程时及时起作用,不是[=222] =] 这里的情况,因为 [Terminal.Gui.Application]::Run()
调用 块 它。
因此,必须使用方法(a)。
回复(一):
C# 以运算符 +=
和 -=
的形式提供 语法糖 用于附加和分离 event-handler 个委托 , 看起来 像 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 为attaching/removing事件处理程序提供了没有这样的语法糖,因此必须直接调用这些方法。
不幸的是,Get-Member
和 tab-completion 都不知道这些方法,而相反,原始事件 names 令人困惑地 do 得到 tab-completed,即使你不能直接对它们采取行动。
Github suggestion #12926旨在解决这两个问题。
用于事件定义的约定:
上面的 EventHandlerType
属性 显示了 event-handler 委托的类型名称,在这种情况下它正确地遵守了 约定 使用基于泛型类型 System.EventHandler<TEventArgs>
的委托,其签名为:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
TEventArgs
表示包含event-specific信息的实例类型。
另一个约定是这样的event-arguments类型是从System.EventArgs
class, which the type at hand, PSInvocationStateChangedEventArgs
派生出来的,是
提供无event-specific信息的事件按照约定使用non-genericSystem.EventHandler
委托:
public delegate void EventHandler(object? sender, EventArgs e);
大概是因为这个代表在历史上曾用于 所有 代表,甚至是那些 with 事件参数 - 在 之前generics 在 .NET 2 中出现 - EventArgs
参数仍然存在,约定是传递 EventArgs.Empty
而不是 null
来表示没有参数。
类似地,long-established 框架类型定义 non-generic custom 委托及其特定的 event-arguments 类型,例如System.Windows.Forms.KeyPressEventHandler
.
None 这些约定由 CLR 强制实施,但是,所定义的相关事件证明了这一点作为 public event Action Clicked;
,它使用 无参数 委托作为事件处理程序。
通常建议遵守约定,以免违背用户期望,尽管这样做有时不太方便。
PowerShell 在使用脚本块 ({ ... }
) 作为委托时非常灵活,值得注意的是 not 强制执行特定的 参数签名 通过param(...)
:
脚本块被接受,无论它是否声明了任何参数,过多或过少,尽管 event-originating 对象实际传递的那些参数确实绑定到 script-block 参数必须是 type-compatible(假设脚本块的参数是明确键入的)。
因此,Steve 的代码:
$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 上,为了使终端 return 终端在退出应用程序后进入可用状态,以下附加操作是需要:
[Terminal.Gui.Application]::Shutdown()
。没有它,在同一个会话中重新调用应用程序将不起作用。tput init
。没有它,后来的 command-line 编辑失败了(值得注意的是,向上和 down-arrow)。
[=177=
Terminal.Gui
类型不是 PowerShell-friendly,原因有二:[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
#
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.