4

I want to be able to create a customized event that will trigger to functions or objects in general that are subscribed to that an event. I have looked at some functions such as: New-Event and Register-ObjectEvent, but I am not entirely sure how to put them together efficiently.

The idea of what I want is for a function to automatically execute and perform an operation automatically when a certain event is raised. For example, I have attached a small program that will - by idea - raise an event (TestEvent::SendMessage()) to subscribers (TestSubscribeEvent::DisplayMessage()). This is only by idea, but I am really unsure as to how I can properly implement this functionality within PowerShell.

I am currently using PowerShell 6.2. If any other information is necessary, please do let me know.

class TestSubscribeEvent
{
    # This function would be subscribed to SendMessage from the class TestEvent.
    [void] DisplayMessage([string] $msg)
    {
        Write-Host "EVENT REPLIED WITH THIS MESSAGE:`r`n$($msg)";
    } # DisplayMessage()
} # CLASS :: TestSubscribeEvent



class TestEvent
{
    [void] SendMessage([string] $msg)
    {
        # SEND THIS MESSAGE VIA EVENTS
        #  Allow all subscribers to see the message
        # Message can be any literal string, such as "Hello".
    } # SendMessage()
} # CLASS :: TestEvent



function main()
{
    # Create a new instance of these objects
    [TestEvent] $testEvent = [TestEvent]::new();
    [TestSubscribeEvent] $testSub = [TestSubscribeEvent]::new();

    # Send the message; TestSubscribeEvent should already be subscribed to TestEvent.
    $testEvent.SendMessage("All these squares make a circle.");
} # main()



# Jump to main, start the program.
main;

In this code, though only by idea, $testSub.DisplayMessage() should automatically execute once $testEvent.SendMessage() raises a new event. The output from $testSub.DisplayMessage() will depend on what message is provided from the $testEvent.SendMessage() via Event Message.

Hopefully this makes sense; if needed - I can elaborate a bit further if requested.

Thank you for your time,

4 Answers4

4

First my apologies for the wall of text. You should look into the New-Event, Register-EngineEvent, Unregister-Event, Get-Event, Get-EventSubscriber, and Remove-Event cmdlets. Here is a quick sample on working with these events to fire off and handle your own events.

First we will setup 2 small functions. This writes some automatic variables to the console:

function Write-EventToConsole ($evt)
{
    Write-Host "Write info to the console from a function"
    Write-Host $Evt.MessageData
    Write-Host $Evt.Sender
    Write-Host $Evt.TimeGenerated      
    Write-Host $Evt.SourceArgs
}

This one just appends them to a file:

function Save-EventToFile ($evt, $filepath)
{
    "Writing to file"                            | Out-File $filepath -Append
    "Message Data   : {0}" -f $Evt.MessageData   | Out-File $filepath -Append
    "Sender         : {0}" -f $Evt.Sender        | Out-File $filepath -Append
    "Time Generated : {0}" -f $Evt.TimeGenerated | Out-File $filepath -Append     
    "Source Args    : {0}" -f $Evt.SourceArgs    | Out-File $filepath -Append
}

Next we will setup 3 event handlers. We will do these 1 at a time, check the subscribers, and fire an event after each one.

Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    Write-EventToConsole $Event    
} -SupportEvent

Next check to see if our event subscriber is there.

Get-EventSubscriber -Force

SubscriptionId   : 1
SourceObject     : 
EventName        : 
SourceIdentifier : MyEventIdentifier
Action           : System.Management.Automation.PSEventJob
HandlerDelegate  : 
SupportEvent     : True
ForwardEvent     : False

Then we fire the event

$null = New-Event -SourceIdentifier "MyEventIdentifier" `
                  -Sender "MyObject" `
                  -EventArguments "Some arguments that I want to pass" `
                  -MessageData "This is some message data I want to pass"

And see the data written to console.

just write to console
This is some message data I want to pass
MyObject
MyObject
4/5/2019 1:54:27 PM
Some arguments that I want to pass
Some arguments that I want to pass

Adding the additional events:

Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    Write-EventToConsole $Event    
} -SupportEvent

Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    Save-EventToFile $Event C:\temp\event.txt
} -SupportEvent

Check the events subscribers:

Get-EventSubscriber -Force

SubscriptionId   : 1
SourceObject     : 
EventName        : 
SourceIdentifier : MyEventIdentifier
Action           : System.Management.Automation.PSEventJob
HandlerDelegate  : 
SupportEvent     : True
ForwardEvent     : False

SubscriptionId   : 2
SourceObject     : 
EventName        : 
SourceIdentifier : MyEventIdentifier
Action           : System.Management.Automation.PSEventJob
HandlerDelegate  : 
SupportEvent     : True
ForwardEvent     : False

SubscriptionId   : 3
SourceObject     : 
EventName        : 
SourceIdentifier : MyEventIdentifier
Action           : System.Management.Automation.PSEventJob
HandlerDelegate  : 
SupportEvent     : True
ForwardEvent     : False

Now if we fire an event:

$null = New-Event -SourceIdentifier "MyEventIdentifier" `
                  -Sender "MyObject" `
                  -EventArguments "Some arguments that I want to pass" `
                  -MessageData "This is some message data I want to pass"

We can see that the 2 event subscribers that write to console fired:

just write to console
This is some message data I want to pass
MyObject
MyObject
4/5/2019 1:54:27 PM
Some arguments that I want to pass
Some arguments that I want to pass

Write info to the console from a function
This is some message data I want to pass
MyObject
4/5/2019 1:54:27 PM
Some arguments that I want to pass

And the event subscriber that wrote to file fired:

Get-Content C:\temp\event.txt

Writing to file
Message Data   : This is some message data I want to pass
Sender         : MyObject
Time Generated : 4/5/2019 1:54:27 PM
Source Args    : Some arguments that I want to pass

Finally to remove the event subscribers you can use:

Get-EventSubscriber -Force | `
    Where-Object { $_.SourceIdentifier -eq 'MyEventIdentifier' } | `
    Unregister-Event -Force

Hopefully this helps explain this a bit. If not, let me know and I will update the answer to address any questions.

Edit: This also works with classes. Here is a quick sample. Note that if you return a value it may not be usable if you use -SupportEvent. Removing SupportEvent will run the action as a PSEventJob. This will let you use Get-Job and Receive-Job to retrieve the value.

# Define a class
class TestSubscribeEvent
{
   # Static method
   static [void] DisplayMessage($evt)
   {
        Write-Host "Written from static handler"
        Write-Host $Evt.MessageData
        Write-Host $Evt.Sender
        Write-Host $Evt.TimeGenerated      
        Write-Host $Evt.SourceArgs
        Write-Host '----------------------------'
   }

   [string] DisplayMessage2($evt)
   {
        return "You wont see this if you use supportevent"
   }

   [void] SetSomeProperty($evt)
   {
        $this.SomeProperty ="Set from internal instance`n`t{0}`n`t{1}`n`t{2}`n`t{3}" -f $Evt.MessageData, $Evt.Sender, $Evt.TimeGenerated, $Evt.SourceArgs
   }

   [string] $SomeProperty
}

# Define a class
class TestEvent
{
   # Static method
   static [void] SendMessage($msg)
   {
        $null = New-Event -SourceIdentifier "MyEventIdentifier" `
                  -Sender "MyObject" `
                  -EventArguments "Sent from static" `
                  -MessageData $msg
   }

   [void] SendMessage2($msg)
   {
        $null = New-Event -SourceIdentifier "MyEventIdentifier" `
                  -Sender "MyObject" `
                  -EventArguments "Sent from instance" `
                  -MessageData $msg
   }
}

Make some objects:

$subObj = New-Object TestSubscribeEvent
$testEvent = New-Object TestEvent

Register some event handlers

#register static handler
Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    [TestSubscribeEvent]::DisplayMessage($event)   
} -SupportEvent

#register instance handler
Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    $subObj.DisplayMessage2($event) 
} -SupportEvent

#register instance handler without Support Event
#this creates a PSEventJob which we swallow with $null, we check it later
$null = Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    $subObj.DisplayMessage2($event) 
}

#register instance handler that updates a property
Register-EngineEvent -SourceIdentifier "MyEventIdentifier" -Action {
    $subObj.SetSomeProperty($event)
} -SupportEvent

Now we will call the event generators:

#call static generator
[TestEvent]::SendMessage("Static called message")

This generates the following to the console:

Written from static handler
Static called message
MyObject
4/9/2019 8:51:20 AM
Sent from static
----------------------------

We can see that it was a static method that was called, that the new event was created in the static method, and that the static handler got it.

We can also use get-job to see the PSEventJob running.

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command                  
--     ----            -------------   -----         -----------     --------             -------                  
47     MyEventIdent...                 Running       True                            ...                      

You can then use Get-Job 47 | Receive-Job -keep to get the output

You wont see this if you use supportevent

Note that the other handler that called the same method that returned a value and did use SupportEvent basically lost all output. No PSEventJob was created so the output had nowhere to go.

We can also see the property was set on the instance object.

$subObj.SomeProperty

Set from internal instance
    Static called message
    MyObject
    4/9/2019 9:05:26 AM
    System.Object[]

The instance version does essentially the same thing all over just passing the different values.

#call instance generator
$testEvent.SendMessage2("Instance called message")

This give the following:

Written from static handler
Instance called message
MyObject
4/9/2019 9:02:02 AM
Sent from instance
----------------------------
StephenP
  • 3,895
  • 18
  • 18
  • Thank you so much and thank you for also appending the changes to the class structure! I have noticed that I was not able to fire the event using PowerShell 6.2 using the POP functions - yet they work just fine in PowerShell already packaged in Windows 10. Though, using the OOP approach seems to work perfectly fine in PowerShell 6.2. Thank you so much again! – Nicholas Gautier Apr 05 '19 at 17:41
  • By chance, the functions within the classes, do they need to be static functions in order to work correctly? I have tried customize the ``Register-EngineEvent``'s Action from ``[TestSubscribeEvent]::DisplayMessage($event)`` to ``$subObj.DisplayMessage($event)`` where $subObj is an instance of TestSubscribeEvent class, but it is unable to properly fire the event as expected. Also, if this needs to be submitted as a new topic - please do let me know. Thanks again. – Nicholas Gautier Apr 05 '19 at 20:12
  • It does work, I'll update the example. As a caveat though the event handler you register with Register-EngineEvent runs as a PSEventJob similar to a background job so if you just write to the output stream you won't get anything in the current scope. You will probably want to utilize properties. – StephenP Apr 08 '19 at 23:15
  • Thank you so much for your help and further assistance! – Nicholas Gautier Apr 18 '19 at 15:35
1

Action Example with Add/Remove via HashSet

This example uses [System.Action[<type list>]] to define an event, and a HashSet to store the subscribers. Subscribers are script blocks cast as an Action, and I'm referring to them as an eventBlock. (My thinking: An event block is a script block that handles events.)

This version automatically sends sender as an [object] to the event block, but there are a couple of squirrelly things I came across that should be considered. The parameter sender is an automatic variable, so VSCode complaines, but PowerShell seems fine with it. But the really odd thing is that the event blocks appears to receive the arguments passed to Invoke, but use the parameter names defined by _OnVariableChanged - that may not be exactly what is happening, but either way, everything works fine when parameter names and arguments are in sync with each other. Which is why OnVariableChanged passes $this, along with the other arguments, to _OnVariableChanged, and then _OnVariableChanged does the actual invoke.

class EventsViaAction {
    $_variableChanged = [System.Collections.Generic.HashSet[System.Action[Object, string, object]]]::new()
    [void]Add_VariableChanged([System.Action[Object, string, object]]$eventBlock) {
        if($null -ne $eventBlock) {
            $null = $this._VariableChanged.Add($eventBlock)
        }
    }
    [void]Remove_VariableChanged([System.Action[Object, string, object]]$eventBlock) {
        $null = $this._VariableChanged.Remove($eventBlock)
    }
    [void]_OnVariableChanged([Object]$sender, [string]$name, [object]$value) {
        foreach ($eventBlock in $this._VariableChanged) {
            $null = $eventBlock.Invoke($sender, $name, $value)
        }
    }
    [void]OnVariableChanged([string]$name, [object]$value) {
        $this._OnVariableChanged($this, $name, $value)
    }
}

This example use adds 3 subscribers, fires the event, removes the 2nd subscriber, and fires the event again:

$EVA = [EventsViaAction]::new()
$A1 = {
    Write-Host "Sender:$($sender.GetType())"
    Write-Host "$name, $value"
}
$A2 = { Write-Host "{$name, $value}" }
$A3 = { Write-Host "[$name, $value]" }
$EVA.Add_VariableChanged($A1)
$EVA.Add_VariableChanged($A2)
$EVA.Add_VariableChanged($A3)
$EVA.OnVariableChanged("Left", "Right")
Write-Host
$EVA.Remove_VariableChanged($A2)
$EVA.OnVariableChanged("left", "right")

Producing this output:

Sender:EventsViaAction
Left, Right
{Left, Right}        
[Left, Right]        

Sender:EventsViaAction
left, right
[left, right]

Func Example with Add/Remove via HashSet

Basically the same as the above example, only using Func instead of Action, which enables returning an array of values from all the subscribers to the caller of the event.

class EventsViaFunc {
    $_ConvertString = [System.Collections.Generic.HashSet[System.Func[Object, string, int]]]::new()
    [void]Add_ConvertString([System.Func[Object, string, int]]$eventBlock) {
        if($null -ne $eventBlock) {
            $null = $this._ConvertString.Add($eventBlock)
        }
    }
    [void]Remove_ConvertString([System.Func[Object, string, int]]$eventBlock) {
        $null = $this._ConvertString.Remove($eventBlock)
    }
    [int[]]_OnConvertString([Object]$sender, [string]$number) {
        $Return = [System.Collections.Generic.List[int]]::new()
        foreach($eventBlock in $this._ConvertString){
            $Return.Add($eventBlock.Invoke($sender, $number))
        }
        return $Return.ToArray()
    }
    [int[]]OnConvertString([string]$number) {
        return $this._OnConvertString($this, $number)
    }
}

This example use passes the number 12345 to the event when firing it, each subscriber preforms a different operations, with each returning an integer, which results in OnConvertString returning an array of integers. Then a foreach loop processes the array, outputting a list of the integers found in the array.

$EVF = [EventsViaFunc]::new()
$F1 = { Write-Host "Sender:$($sender.GetType())";return $number.Length}
$F2 = { return [int]$number}
$F3 = { return ([char[]]$number | ForEach-Object { [int][string]$_ } | Measure-Object -Sum).Sum }
$EVF.Add_ConvertString($F1)
$EVF.Add_ConvertString($F2)
$EVF.Add_ConvertString($F3)
$N = 0
foreach ($Result in $EVF.OnConvertString("12345")) {
    Write-Host "$((++$N)): $Result"
}

Producing this output.

Sender:EventsViaFunc
1: 5
2: 12345
3: 15
Darin
  • 1,423
  • 1
  • 10
  • 12
0

Real World Minimal Example (Mimicking C# Add_EventName design):

Changing the value of a variable, and notifying code elsewhere of the change.

The VariableEventArgs class holds the name and value of the variable that is passed to the event handler.

class VariableEventArgs: EventArgs {
    [string]$Name
    [object]$Value
    VariableEventArgs([string]$name, [object]$value) {
        $this.Name = $name
        $this.Value = $value
    }
}

Use Add_VariableChanged method of PSEvents class to subscribe to notifications of changes to variables. Use PSEvents' OnVariableChanged method to fire the event.

class PSEvents {
    hidden [string]$_GUID
    hidden [string]$_GUIDVC
    hidden [string]GenEventGuid([string]$eventName) {
        return "$($this._GUID).$eventName"
    }
    PSEvents() {
        $this._GUID = (New-Guid).ToString()
        $this._GUIDVC = $this.GenEventGuid('VariableChanged')
    }
    [void]Add_VariableChanged([ScriptBlock]$ScriptBlock) {
        Register-EngineEvent -SourceIdentifier $this._GUIDVC -Action $ScriptBlock
    }
    [void]OnVariableChanged([string]$name, [object]$value) {
        [void](New-Event -SourceIdentifier $this._GUIDVC -Sender $this -EventArguments ([VariableEventArgs]::new($name, $value)))
    }
}

Example use:

This example:

  1. Defines $TestVar as "ABC".
  2. Creates an instances of PSEvents.
  3. Sets up the first subscriber to change the variable named in $e.Name to value in $e.Value, while providing info on $TestVar prior to the change, and info on parameters received.
  4. Sets up the second subscriber to view the current value of $TestVar, and info on parameters received.

This example use assumes that the first event registered is called before all other events, while this works for this example, I didn't notice any documentation explicitly stating what order events are fired. So don't assume this firing order unless you find documentation on it.

$TestVar = 'ABC'
$PSEs = [PSEvents]::new()
$PSEs.Add_VariableChanged({
    $e = $EventArgs
    Write-Host "`$TestVar=$TestVar"
    New-Variable -Name $e.Name -Value $e.Value -Force -Scope Global
    Write-Host "{$($sender.GetGuid()), $($e.Name), $($e.Value)}"
})
$PSEs.Add_VariableChanged({
    $e = $EventArgs
    Write-Host "`$TestVar=$TestVar"
    Write-Host "[$($sender.GetGuid()), $($e.Name), $($e.Value)]"
})

This command actually fires the VariableChanged event:

$PSEs.OnVariableChanged("TestVar", "XYZ")

Example Output:

$TestVar=ABC
{973e7abd-e5fb-4bac-b63c-5106c0ecf2bb, TestVar, XYZ}
$TestVar=XYZ
[973e7abd-e5fb-4bac-b63c-5106c0ecf2bb, TestVar, XYZ]

Reason behind giving this example long after question was answered:

There seems to be very few good examples of how to use Register-EngineEvent and New-Event, and nearly all are variations of the same code. Maybe placing this example here will help save new programmers some time.

NOTE:

PSEvents class has only very basic functionality, but should make a good foundation to build on.

Also, some of the documentation suggested that method names starting with Add_ are reserved, so I'm surprised the code works - but you may want to change it to an alternative naming format to avoid risk of problems.

Darin
  • 1,423
  • 1
  • 10
  • 12
0

Inheriting from C# class with events predefined.

As of this time, PowerShell classes must have all external references fully loaded prior to the script being parsed. The problem is, the actual loading of external references doesn't happen until a later execution stage. To learn more, read this answer.

There are workarounds, and this one involves placing the PowerShell code in a CMD file (such as MyBatchFile.CMD). For those not familiar with CMD files, they can be ran from Windows command prompt or double clicked from Windows Explorer. If ran from Windows Explorer, replace the REM PAUSE line with PAUSE, otherwise, the window will close as soon as the script exits.

For this trick to work, the CMD file must start with a batch language bootstrap section of code that calls PowerShell, and tells PowerShell to load the current CMD file in memory as a script block and execute it. The following block of code actually saves the code for the C# class called csTrafficLight in an environment variable called CSCode, and uses Add-Type to load the class prior to the loading and executing of the current CMD file as a script block by PowerShell.

<# :
@ECHO OFF
    SET CLA=%*
    SET f0=%~f0
    SET CMDScriptRoot=%~dp0
    SET CSCode=using System;public class csTrafficLight{public event EventHandler Red, Yellow, Green;public void OnRed(EventArgs e){Red(this,e);}public void OnYellow(EventArgs e){Yellow(this,e);}public void OnGreen(EventArgs e){Green(this,e);}}
    PowerShell -NoProfile -ExecutionPolicy RemoteSigned -Command "Add-Type -Language 'CSharp' -TypeDefinition $Env:CSCode;.([scriptblock]::Create((get-content -raw $Env:f0)))"
    REM PAUSE
GOTO :EOF
<#~#>

Do not use the following block of code, this is the c# code taken from the environmental variable CSCode in the above code. It is expanded out here to make it easier to read. As you can see, 3 events are define and also 3 OnEvent methods are defined to raise the events. Unfortunately, I never did figure out how to raise the events directly from PowerShell, so that is why these methods are defined.

using System;
public class csTrafficLight{
    public event EventHandler Red, Yellow, Green;
    public void OnRed(EventArgs e){
        Red(this,e);
    }
    public void OnYellow(EventArgs e){
        Yellow(this,e);
    }
    public void OnGreen(EventArgs e){
        Green(this,e);
    }
}

The code line <#~#> is where the batch language bootstrap code ends and the PowerShell script begins. This first section declares the EventArgs class that will be used for messages when the events are raised.

class TrafficLightInfo: EventArgs {
    [int]$CarCount
    [int]$Violators
    TrafficLightInfo([int]$carCount) {
        $this.CarCount = $carCount
    }
    TrafficLightInfo([int]$carCount, [int]$violators) {
        $this.CarCount = $carCount
        $this.Violators = $violators
    }
}

This second block of code defines a class, psTrafficLight, that inherits from the C# class csTrafficLight and defines 3 methods for raising the events that do the work of creating instances of [TrafficLightInfo] class used to send the messages. It also includes a DoEvents() method to emulate raising the events in a real world situation.

class psTrafficLight: csTrafficLight {
    [void]OnRed([int]$carCount, [int]$violators) {
        ([csTrafficLight]$this).OnRed([TrafficLightInfo]::new($carCount, $violators))
    }
    [void]OnYellow([int]$carCount) {
        ([csTrafficLight]$this).OnYellow([TrafficLightInfo]::new($carCount))
    }
    [void]OnGreen([int]$carCount) {
        ([csTrafficLight]$this).OnGreen([TrafficLightInfo]::new($carCount))
    }
    [void]DoEvents() {
        $this.OnRed(5, 0)
        Start-Sleep -Seconds 5
        $this.OnGreen(9)
        Start-Sleep -Seconds 1
        $this.OnYellow(1)
        Start-Sleep -Seconds 5
        $this.OnRed(3, 1)
        Start-Sleep -Seconds 5
        $this.OnGreen(8)
    }
}

This code creates an instance of psTrafficLight, defines 2 subscribers to the Red event, 1 for the Yellow event, and 1 for the Green event, followed by calling the DoEvents() method.

$TrafficLight = [psTrafficLight]::new()
$TrafficLight.Add_Red({
    param([object]$sender, [TrafficLightInfo]$e)
    Write-Host "$($e.CarCount) cars waiting at red light." -ForegroundColor Red
})
$TrafficLight.Add_Red({
    param([object]$sender, [TrafficLightInfo]$e)
    Write-Host "$($e.Violators) cars ran the red light." -ForegroundColor Red
})
$TrafficLight.Add_Yellow({
    param([object]$sender, [TrafficLightInfo]$e)
    Write-Host "$($e.CarCount) cars passed through yellow light." -ForegroundColor Yellow
})
$TrafficLight.Add_Green({
    param([object]$sender, [TrafficLightInfo]$e)
    Write-Host "$($e.CarCount) cars passed through green light." -ForegroundColor Green
})
$TrafficLight.DoEvents()

And this is the output of the above code. If ran from the Windows command prompt, it will show each line in the color matching the color of the traffic light.

5 cars waiting at red light.
0 cars ran the red light.
9 cars passed through green light.
1 cars passed through yellow light.
3 cars waiting at red light.
1 cars ran the red light.
8 cars passed through green light.
Darin
  • 1,423
  • 1
  • 10
  • 12