3

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?

Martin Maat
  • 714
  • 4
  • 23
  • 1
    `Receive-Job ` – user4003407 Jan 24 '17 at 11:13
  • Oh wel... this does give me the "return value" of the handler but the child module raising the event does not know about the job (the job is the result of the Register-EngineEvent call which is in the parent module, I would in this case have liked the job to be the result of New-Event instead which is a child-module thing). So this seems kind of pointless, nice for running background tasks but not for an event system. Am I looking at the wrong PowerShell feature for my needs? – Martin Maat Jan 24 '17 at 11:31
  • *Am I looking at the wrong PowerShell feature for my needs?* IMHO, yes. If you want, that code from parent module be callable from child module, then make it to be global function. And call this global function instead of raising events. – user4003407 Jan 25 '17 at 19:52
  • @PetSerAl Thank you for your correct answers/comments, I appreciate them. I am however looking for something that better addresses my inital concerns so I started a bounty hoping to hear about alternatives. – Martin Maat Jan 26 '17 at 16:46

1 Answers1

0

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!

Community
  • 1
  • 1
Martin Maat
  • 714
  • 4
  • 23