Skip to content

Improve discoverability and support for (de)registering event handlers #12926

@vexx32

Description

@vexx32

Summary of the new feature/enhancement

Currently, finding event metadata is pretty difficult in PS. The event members are deliberately hidden from autocompletion, can't be directly accessed from objects like methods can to reveal metadata which is often useful in determining how you want to work with them, and aren't available for autocompletion.

Additionally, while Register-EngineEvent and Register-ObjectEvent do exist, they won't always work. For example, using some GUI-centric script patterns, many users can run into deadlocks using Register-ObjectEvent and some Forms classes.

To work around this, many scripters resort to using the hidden $obj.add_EventName($handler) methods which are not discoverable, not well-documented (as they are generally considered an implementation detail), and difficult to work with in PS.

Most documentation around events is C#-centric and will recommend the C# syntax for registering and deregistering delegates; obj.EventName += eventHandler;

This creates friction and confusion; users have neither appropriate access to event metadata to determine how to work with them in PS, nor appropriate documentation to fill in the gaps where Register-ObjectEvent isn't a workable solution. Additionally, even discovering event names for use with Register-ObjectEvent is difficult since the members aren't directly available.

Proposed technical implementation details (optional)

  1. Expose event members as actual object members, allowing their names to register for tab completion, and the PSEvent metadata to be shown to users accessing the event.
  2. Implement a custom binder for use with += / -= which bridges the gap by calling the appropriate add_* / remove_* method for the targeted event.
  3. Add some logic into the Compiler to perform a runtime check for LHS being a PSEvent when handling expressions with += or -= operators (this will be dependent on Update readme #1 being implemented, or this check cannot be relied on). If the LHS is PSEvent, call out to the binder from Import issues from VSO #2 and (de)register the event. Naturally, fall back to standard += behaviour if the LHS is not PSEvent.

Demonstration

PS> $ps = [powershell]::Create()
PS> $handler = { Write-Host "event triggered!" }
# Tab completion lists the event member as well
PS> $ps.Invo
InvocationStateInfo     Invoke                  InvokeAsync             InvocationStateChanged  
PS> $ps.InvocationStateChanged += $handler
PS> $ps.AddScript('Write-Output "data from pipe"').Invoke()
event triggered!
event triggered!
data from pipe
PS> $ps.InvocationStateChanged -= $handler
PS> $ps.AddScript('Write-Output "data from pipe"').Invoke()
data from pipe

I have working code for this that I can submit as a PR if there is interest in having this be a part of PowerShell. Just have to put some tests together 😎

Below illustrates the metadata event members would show if users attempt to examine them, once the aforementioned changes are in place.

PS> $ps.InvocationStateChanged

MemberType      : Event
Value           : System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs] InvocationStateChanged
TypeNameOfValue : System.Management.Automation.PSEvent
Name            : InvocationStateChanged
IsInstance      : True


PS /Users/joelfrancis/repos/Github/powershell> $ps.InvocationStateChanged.Value

MemberType       : Event
Name             : InvocationStateChanged
DeclaringType    : System.Management.Automation.PowerShell
ReflectedType    : System.Management.Automation.PowerShell
MetadataToken    : 335544336
Module           : System.Management.Automation.dll
Attributes       : None
IsSpecialName    : False
AddMethod        : Void add_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
RemoveMethod     : Void remove_InvocationStateChanged(System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs])
RaiseMethod      : 
IsMulticast      : True
EventHandlerType : System.EventHandler`1[System.Management.Automation.PSInvocationStateChangedEventArgs]
CustomAttributes : {}
IsCollectible    : True


PS> $ps.InvocationStateChanged.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    PSEvent                                  System.Management.Automation.PSMemberInfo

PS> $ps.InvocationStateChanged.Value.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    False    RuntimeEventInfo                         System.Reflection.EventInfo

Metadata

Metadata

Assignees

No one assigned

    Labels

    In-PRIndicates that a PR is out for the issueIssue-Enhancementthe issue is more of a feature request than a bugResolution-No ActivityIssue has had no activity for 6 months or moreWG-Enginecore PowerShell engine, interpreter, and runtime

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions