tig*_*tig 2 .net powershell event-handling terminal.gui
Terminal.Gui (gui.cs)提供了Button一个Clicked定义为事件的类:
public event Action Clicked;
Run Code Online (Sandbox Code Playgroud)
我正在尝试在 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()
Run Code Online (Sandbox Code Playgroud)
Clicked =上面示例中的赋值返回错误:
InvalidOperation: The property 'Clicked' cannot be found on this object. Verify that the property exists and can be set.
但是智能感知Clicked为我自动完成......所以我猜这是一个类型问题?
我找不到任何 PowerShell 文档,[Action]而且我发现的其他示例也没有给我带来任何乐趣。
如何Action在 PowerShell 中为基于 dotnet 的事件定义事件处理程序?
mkl*_*nt0 17
史蒂夫·李(Steve Lee)的有益回答提供了关键的指导;让我补充一下背景信息:
PowerShell 提供两种基本的事件订阅机制:
(a) .NET-native,如 Steve 的回答所示,您可以通过实例方法将脚本块( { ... }) 作为委托附加到对象的<Name>事件.add_<Name>()(委托是一段用户提供的回调代码,当事件触发) - 请参阅下一节。
(b) PowerShell 介导的,使用Register-ObjectEvent和相关 cmdlet:
-Action。Get-Event。方法 (b) 的回调方法仅在PowerShell控制前台线程时及时起作用,但这里的情况并非[Terminal.Gui.Application]::Run()如此,因为调用会阻止它。因此,必须使用方法(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]
Run Code Online (Sandbox Code Playgroud)
PowerShell没有提供用于附加/删除事件处理程序的语法糖,因此必须直接调用这些方法。
不幸的是,Get-Member制表符完成和制表符补全都不知道这些方法,而相反,原始事件名称令人困惑地确实得到了制表符完成,即使您无法直接对它们进行操作。
Github 建议 #12926旨在解决这两个问题。
用于事件定义的约定:
上面的属性EventHandlerType显示了事件处理程序委托的类型名称,在本例中,它正确地遵守了使用基于泛型类型的委托的约定System.EventHandler<TEventArgs>,其签名为:
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
Run Code Online (Sandbox Code Playgroud)
TEventArgs表示包含事件特定信息的实例的类型。另一个约定是,此类事件参数类型派生自类System.EventArgs,即手头的类型PSInvocationStateChangedEventArgs, 。
按照惯例不提供特定于事件的信息的事件使用非通用委托:System.EventHandler
public delegate void EventHandler(object? sender, EventArgs e);
Run Code Online (Sandbox Code Playgroud)
据推测,因为该委托历史上用于所有委托,甚至对于那些具有事件参数的委托(在 .NET 2 中出现泛型EventArgs之前) ,参数仍然存在,并且约定是传递EventArgs.Empty而不是null表示参数不存在。
类似地,长期建立的框架类型使用其特定的事件参数类型定义非通用自定义System.Windows.Forms.KeyPressEventHandler委托,例如。
然而,这些约定都不是由 CLR强制执行的,所讨论的事件被定义为public event Action Clicked;,它使用无参数委托作为事件处理程序。
通常建议遵守约定,以免违背用户的期望,尽管这样做有时不太方便。
PowerShell 在使用脚本块 ( { ... }) 作为委托方面非常灵活,并且值得注意的是,它不通过以下方式强制执行特定的参数签名param(...):
无论脚本块是否声明任何、太多或太少的参数,脚本块都会被接受,尽管那些绑定到脚本块参数的事件发起对象实际传递的参数必须是类型兼容的(假设脚本块的参数是显式键入的)。
因此,史蒂夫的代码:
$btn.Add_Clicked({
param($sender, $e)
[Terminal.Gui.Application]::RequestStop()
})
Run Code Online (Sandbox Code Playgroud)
尽管有无用的参数声明,但仍然有效,因为没有任何参数被传递到脚本块,并且System.Action委托类型是无参数的。
以下内容就足够了:
$btn.Add_Clicked({
[Terminal.Gui.Application]::RequestStop()
})
Run Code Online (Sandbox Code Playgroud)
注意:即使没有声明参数,您也可以通过自动$this变量(在本例中与 相同)引用事件发送者(触发事件的对象$btn)。
简化的示例代码:
[Terminal.Gui.Application]::Shutdown()退出应用程序后,为了将终端返回到可用状态,进行调用很重要
至少其中一种Terminal.Gui类型不适合 PowerShell:
[string],而是作为[NStack.ustring];实现的。虽然您可以[string]透明地使用实例来分配此类属性,但再次显示它们会执行枚举并单独呈现底层字符的代码点。
.ToString(); 例如$btn.Text.ToString()从 PowerShell 7.3.2 开始,没有与 NuGet 包的直接集成,因此将已安装包的程序集加载到 PowerShell 会话中非常麻烦 - 请参阅此答案,它展示了如何使用.NET Core SDK下载包并使其依赖项可用。
在PowerShell (Core) 7.2+中,可以在这种情况下解决该问题:Microsoft.PowerShell.ConsoleGuiTools模块 附带 Terminal.Gui.dll,因此您可以安装该模块,并在那里引用 DLL。
请注意,Add-Type -AssemblyName仅适用于当前目录(而不是脚本目录)中的程序集或 PowerShell 本身 (PowerShell [Core] v6+) 附带的程序集/位于 GAC (Windows PowerShell) 中的程序集。
鉴于目前从 PowerShell 使用 NuGet 包的麻烦程度,GitHub 功能建议 #6724要求Add-Type进行增强以直接支持 NuGet 包。
using namespace Terminal.Gui
# Load the Terminal.Gui.dll assembly, if necessary.
if (-not ('Terminal.Gui.Application' -as [Type])) {
if ($PSVersionTable.PSVersion -ge '7.2') {
# Load the Terminal.Gui assembly via the 'Microsoft.PowerShell.ConsoleGuiTools'
# module, by installing that module on demand.
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
}
# Terminal.Gui.dll is inside the module's folder.
try { Add-Type -LiteralPath (Join-Path (Get-Module -ListAvailable Microsoft.PowerShell.ConsoleGuiTools).ModuleBase Terminal.Gui.dll) } catch { throw }
}
else {
# Windows PowerShell (or earlier PS Core versions)
# Unfortunately, there's no easy way to gain access to Terminal.Gui.dll, and the
# best option is to use an aux. NET SDK project as shown in /sf/answers/3500329451/
# The next command assumes that the steps there have been followed.
try { Add-Type -Path $HOME\.nuget-pwsh\packages-winps\terminal.gui\*\Terminal.Gui.dll } catch { throw }
}
}
# 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 that isused 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()
# Required to restore the previous terminal screen
# and for being able to rerun the application in the same session.
[Application]::Shutdown()
Run Code Online (Sandbox Code Playgroud)
C# 代码将添加一个 lambda:
btn.Clicked += ...
Run Code Online (Sandbox Code Playgroud)
所以在 PowerShell 中,需要显式调用 Add_Clicked() 方法:
$btn.Add_Clicked({
param($sender,$e)
[Terminal.Gui.Application]::RequestStop()
})
Run Code Online (Sandbox Code Playgroud)
尽管在本示例中未使用,但参数与方法签名匹配。