Thursday, February 2, 2017

How to get a return value from an engine event handler in PowerShell?

Leave a Comment

I want to use events in PowerShell to separate responsibilities and eliminate dependencies between modules. A child module needs to notify a parent module about its activity and sometimes it needs information in return from the parent module when it raises an event.

This is what I have tried so far. In a real-world scenario I would put the registration-, unregistration and handler function parts in a parent module and the New-Event parts in a child module.

function MyHandler {    param (     [System.Management.Automation.PSEventArgs]$Event   )    Write-Host "In MyHandler now!" -ForegroundColor Yellow    Write-Host "Sender        : $($Event.Sender)"    Write-Host "SourceArgs    : $($Event.SourceArgs)"    Write-Host "MessageData   : $($Event.MessageData)"   Write-Host "TimeGenerated : $($Event.TimeGenerated)"    $Event.MessageData = "This does not make it back."    return "Neither does this." }  # subscribe [String]$MessageData = "Input" Register-EngineEvent -SourceIdentifier Pipo -Action {MyHandler $Event }  # raise event and see what we get back New-Event -Sender "Me" -SourceIdentifier "Pipo" -EventArguments "MyEventArgs" -MessageData $MessageData  # cleanup Unregister-Event -SourceIdentifier Pipo 

This does trigger the call to MyHandler and I can access passed arguments and event properties. The problem is I cannot find a way to get data back from the event handler function to the raiser of the event. My mindset is probably too much C#-oriented, I may be trying to do things in a way that I should go about differently in PS. The whole concept of creating an object and send it down the pipeline in order to raise an event seems odd to me and I do not understand what really happens.

What is the right way to do this?

1 Answers

Answers 1

I found a way that addresses my needs. It turns out I was searching for the wrong keywords, when searching for PowerShell and callbacks I found this question which helped me a lot (particularly Duncan's answer):

Pass a function as a parameter in PowerShell

I worked it into a full example. This is a client script, to be saved as "Client.ps1":

Import-Module -Name ".\Server.psm1" -DisableNameChecking  $script:ClientVar = "Not seen by server, returned by event handler."  function Handle_Initialized {   Write-Host "Handler Initialized is being called."    # a return value is optional   return "=== $script:ClientVar ===" }  function Handle_ProcessedData {    param (     $Argument1,     $Argument2,     $Argument3   )    Write-Host "Handler ProcessedData is called."   Write-Host "Arguments are $argument1, $argument2 and $argument3."    # a return value is optional   return "=== $argument1, $argument2, $argument3 ===" }   Subscribe-Event -Name Initialized  -Handler $function:Handle_Initialized Subscribe-Event -Name ProcessedData -Handler $function:Handle_ProcessedData  Write-Host "" Write-Host "calling with active subscriptions" Write-Host "================================="  Do-ServerStuff  Unsubscribe-Event -Name Initialized Unsubscribe-Event -Name ProcessedData  Write-Host "" Write-Host "calling again with no active subscriptions" Write-Host "================================="  Do-ServerStuff  Remove-Module -Name "Server" 

Then, in the same folder, put this as "Server.psm1":

[ScriptBlock]$script:Handler_Initialized   = $null [ScriptBlock]$script:Handler_ProcessedData = $null  function Subscribe-Event {    param (     [String]$Name,     [ScriptBlock]$Handler   )    switch ($Name) {     Initialized   { $script:Handler_Initialized   = $Handler }     ProcessedData { $script:Handler_ProcessedData = $Handler }   } }  function Unsubscribe-Event {    param (     [String]$Name   )    switch ($Name) {     Initialized   { $script:Handler_Initialized   = $null }     ProcessedData { $script:Handler_ProcessedData = $null }   } }  function Raise-Initialized {    param (   )    if ($script:Handler_Initialized) {     return & $script:Handler_Initialized   } }  function Raise-ProcessedData {    param (     [Object]$Argument1,     [Object]$Argument2,     [Object]$Argument3   )    if ($script:Handler_ProcessedData) {     return & $script:Handler_ProcessedData -Argument1 $Argument1 -Argument2 $Argument2 -Argument3 $Argument3   } }  function Do-ServerStuff {   Write-Host "Before raising event Initialized."   Raise-Initialized   Write-Host "After raising event Initialized."   Write-Host ""   Write-Host "Before raising event ProcessedData."   Raise-ProcessedData -Argument1 "AAA" -Argument2 "BBB" -Argument3 "CCC"   Write-Host "After raising event ProcessedData." } 

And you will find that you have an extensible synchronous event handling system.

Most of the plumbing is in the server module which dictates the prototypes for the handler functions. The client code merely subscribes to and unsubscribes from events and provides the handler implementations. It supports named parameters for all handlers as well as return values (really output values) in the usual PowerShell way. The server knows nothing about any of its clients which I like a lot, dependencies go one way only.

For example, this scheme allows you to build modules that perform pure core logic and nothing else. Rather than logging through some global object you can have the server module raise an event that sends a message and the client can then decide what to do with it and where to send it. This increases the usability of server modules and makes them testable.

It may be a matter of taste, I typically favor using events over dependency injection. With dependency injection the server still needs to know about the type that is injected, with events this is not the case.

Enjoy!

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment