Steve Lee的有用回答提供了重要的指针;让我来补充
背景信息:
PowerShell提供两种基本的事件订阅机制:
(a)
.NET原生,如Steve的答案所示,其中您通过
.add_<Name>()
实例方法将
脚本块(
{ ... }
)作为
委托附加到对象的
<Name>
事件(委托是用户提供的回调代码片段,当事件触发时会被调用)- 请参见下一节。
(b)
由PowerShell介导,使用
Register-ObjectEvent
及相关cmdlets:
传递脚本块给-Action
参数,即可使用类似(a)的基于回调的方法。
或者,可以通过Get-Event
cmdlet按需检索排队的事件。
(b)方法的回调方式仅在
PowerShell控制前台线程的时候才能及时运行,而这里并不是这种情况,因为
[Terminal.Gui.Application]::Run()
调用会
阻塞它。
因此,必须使用(a)方法。
Re (a):
C#提供了语法糖,即+=
和-=
操作符,用于附加和分离事件处理程序委托,它们看起来像赋值,但实际上被转换为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
和制表符自动完成都不知道这些方法,反之,原始事件名称会被自动完成,即使你不能直接对它们进行操作,这很令人困惑。
Github建议#12926旨在解决这两个问题。
事件定义使用的约定:
上面的EventHandlerType
属性显示了事件处理程序委托的类型名称,在这种情况下,它正确地遵循了基于通用类型System.EventHandler<TEventArgs>
的委托的约定,其签名为:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
TEventArgs
代表包含事件特定信息的实例类型。
另一个惯例是这种事件参数类型派生自System.EventArgs
类,而手头的类型PSInvocationStateChangedEventArgs
也是如此。
按照惯例,不提供任何事件特定信息的事件使用非泛型System.EventHandler
委托:
public delegate void EventHandler(object? sender, EventArgs e);
据推测,因为这个委托历史上被用于所有委托,甚至是那些带有事件参数的委托——直到.NET 2中引入泛型之前——所以仍然存在一个EventArgs参数,并且约定是传递EventArgs.Empty而不是null来表示没有参数。
同样,长期使用的框架类型定义了非泛型的自定义委托,具有其特定的事件参数类型,例如
System.Windows.Forms.KeyPressEventHandler。
然而,这些约定都不受CLR的强制执行,这可以从事件被定义为public event Action Clicked;
,使用无参数的委托作为事件处理程序中看出。
通常最好遵循这些约定,以免违反用户的期望,尽管这样做有时不太方便。
PowerShell在使用脚本块({ ... }
)作为委托时非常灵活,它显然不通过param(...)
来强制执行特定的参数签名:
无论脚本块是否声明了任何参数,太多或太少的参数,都可以接受该脚本块,尽管那些实际上由事件发起对象传递并绑定到脚本块参数的参数必须与类型兼容(假设脚本块的参数已明确指定类型)。
因此,Steve的代码:
$btn.Add_Clicked({
param($sender, $e)
[Terminal.Gui.Application]::RequestStop()
})
尽管参数声明是无用的,仍然可以工作,因为从未向脚本块传递任何参数,并且 System.Action
委托类型是 无参 的。
以下内容已足够:
$btn.Add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
注意:即使没有声明参数,您也可以通过
自动$this
变量(在这种情况下与
$btn
相同)引用事件发送器(触发事件的对象)。
简化的示例代码:
在退出应用程序后,调用[Terminal.Gui.Application]::Shutdown()
以使终端返回可用状态是很重要的。
至少有一个Terminal.Gui
类型不适合PowerShell:
- 概念上是文本属性的东西没有实现为
[string]
类型,而是[NStack.ustring]
;虽然可以使用[string]
实例透明地分配给这样的属性,但再次显示它们会执行枚举并单独呈现底层字符的代码点。
- 解决方法:调用
.ToString()
;例如:$btn.Text.ToString()
截至PowerShell 7.3.2,没有与NuGet包直接集成,因此将安装包的程序集加载到PowerShell会话中相当麻烦 - 参见this answer,其中展示了如何使用.NET Core SDK下载包并使其依赖项可用。
using namespace Terminal.Gui
if ($PSVersionTable.PSVersion -ge '7.2') {
if (-not (Get-Module -ListAvailable Microsoft.PowerShell.ConsoleGuiTools)) {
Write-Verbose -Verbose "Installing module Microsoft.PowerShell.ConsoleGuiTools on demand, in the current user's scope."
Install-Module -Scope CurrentUser -ErrorAction Stop Microsoft.PowerShell.ConsoleGuiTools
}
try { Add-Type -LiteralPath (Join-Path (Get-Module -ListAvailable Microsoft.PowerShell.ConsoleGuiTools).ModuleBase Terminal.Gui.dll) } catch { throw }
}
else {
try { Add-Type -Path C:\Users\jdoe\.nuget-pwsh\packages-winps\terminal.gui\*\Terminal.Gui.dll } catch { throw }
}
[Application]::Init()
$win = [Window] @{
Title = 'Hello World'
}
$btn = [Button] @{
X = [Pos]::Center()
Y = [Pos]::Center()
Text = 'Quit'
}
$win.Add($btn)
[Application]::Top.Add($win)
$btn.add_Clicked({
[Application]::RequestStop()
})
[Application]::Run()
[Application]::Shutdown()
$this
的工作方式也存在问题(https://github.com/MicrosoftDocs/PowerShell-Docs/issues/8207#issuecomment-940127593)。 - mklement0$sender
和$e
。所有程序员只需要记住在C#中使用的参数名称前面加上$符号即可。基于$this
的问题以及我现在在Register-EngineEvent
和New-Event
中遇到的问题,我想我会通过param()
或根据需要直接赋值来标准化我的代码,使用$sender
和$e
。感谢您抽出时间回复我的评论,祝您晚上/白天愉快。 - Darin