Wensveen's Blog

November 12, 2012

Synchronous .NET event handling with PowerShell

Filed under: Uncategorized — Tags: , , , — wensveen @ 11:12 pm

There are multiple ways in Windows PowerShell to handle events generated by objects in the .NET framework. The most common, or at least, the most documented way to register an event handler to a .NET object is with Register-ObjectEvent. Usage is fairly straightforward: Register-ObjectEvent -InputObject $obj -EventName "BeforeProcess" -Action { Write-Host "BeforeProcess called" }.

The thing is, that the action you registered with Register-ObjectEvent is called asynchronously. This means that the code that generates the BeforeProcess event doesn’t have to wait for the action to end. Consider, for example, a .NET class:

namespace Echo
{
	public class Echo
	{
		public event EventHandler<EchoEventArgs> BeforeEcho;

		public void EchoMessages(string messages)
		{
			foreach (string message in messages.Split(','))
			{
				EchoEventArgs args = new EchoEventArgs { Message = message };
				if (BeforeEcho != null)
				{
					BeforeEcho(this, args);
				}

				Console.WriteLine(message);
			}
		}
	}

	public class EchoEventArgs : EventArgs
	{
		public string Message { get; set; }
	}
}

Now, create an Echo object in PowerShell:

[void] [System.Reflection.Assembly]::LoadFrom('echo.dll')
$echoObject = New-Object Echo.Echo

Running it without event handlers:

$echoObject.EchoMessages('Mary,had,a,little,lamb')

should yield:
Mary
had
a
little
lamb

Now, when you attach an event handler with Register-ObjectEvent:

Register-ObjectEvent -InputObject $echoObject -EventName "BeforeEcho" -Action { Write-Host ("ObjectEvent: " + $EventArgs.Message) }
$echoObject.EchoMessages('Foo,bar')

Result:
Foo
bar
ObjectEvent: Foo
ObjectEvent: bar

As you can see, the foreach loop in EchoMessages doesn’t wait for the Action to finish, before going on to the next message.

Very often, this is exactly what you want. A lot of examples use System.Timers.Timer, where you just want to execute something every so-and-so hours. But once in a while, you may need the event handler to finish before the next line of code is executed.

Let’s say we want the event handler to be able to skip certain messages, or cancel the rest of the messages altogether. Or maybe modify the message before it is written.

namespace Echo
{
	public class Echo
	{
		public event EventHandler<EchoEventArgs> BeforeEcho;

		public void EchoMessages(string messages)
		{
			foreach (string message in messages.Split(','))
			{
				EchoEventArgs args = new EchoEventArgs { Message = message };
				if (BeforeEcho != null)
				{
					BeforeEcho(this, args);
				}

				if (args.Cancel)
				{
					break;
				}
				else if (!args.Skip)
				{
					Console.WriteLine(args.Message);
				}
			}
		}
	}

	public class EchoEventArgs : EventArgs
	{
		public string Message { get; set; }
		public bool Skip { get; set; }
		public bool Cancel { get; set; }
	}
}

Now, let’s say I want to cancel all messages straight away:

Register-ObjectEvent -InputObject $echoObject -EventName "BeforeEcho" -Action { $EventArgs.Cancel = $true }
$echoObject.EchoMessages('1,2,3,4,5')

Result:
1
2
3
4
5

Why? Because you’re already too late. When you decide to cancel, the number is already outputted. Next invocation is with a new EchoEventArgs object. So we need to register the event handler for synchronous execution. Unfortunately, it isn’t documented at all how to do this. Looking with Reflector, we can see the compiler generated two methods for us: add_BeforeEcho and remove_BeforeEcho:
Echo class in Reflector

So, when we register the event handler with add_BeforeEcho, it all works as expected:

$echoObject.add_BeforeEcho({
  param($Source, $EventArgs)
  if ($EventArgs.Message -eq '2') {
    $EventArgs.Skip = $true
  } elseif ($EventArgs.Message -eq '7') {
    $EventArgs.Cancel = $true
  } else {
    $EventArgs.Message = ($EventArgs.Message + ".00")
  }
})
$echoObject.EchoMessages('1,2,3,4,5,6,7,8,9,10')

Result:
1.00
3.00
4.00
5.00
6.00

Hurray!

Advertisements

5 Comments »

  1. Thank you! Your example helped me solve my problem!

    Comment by Nick — November 26, 2012 @ 11:11 pm

  2. After struggling with a problem for the past few days this turned out to be the solution. Unfortunately I came across your post way earlier, too, but didn’t make the connection that it was the source of my problem. It didn’t help that those add_ functions don’t show in the | Get-Member output so I thought it wasn’t relevant šŸ˜„

    Comment by Cody Konior — January 16, 2013 @ 2:11 am

    • The danger with those generated methods is that they aren’t guaranteed to exist. Future versions of the compiler might name them differently (therewith breaking all and any backward compatibility, so they probably won’t unless they have a very good reason). .NET Reflector is a nice tool to see what is available under the hood. Calling non-public members are tricky though, because you’ll need to use reflection.
      Cheers,
      Matthijs

      Comment by wensveen — January 16, 2013 @ 8:41 am

  3. 4 years later…

    I had subscribed to an object with Register-ObjectEvent, passed in a -MessageData array, and had a little scriptblock which would output data into that. After dealing and finishing with the object that triggers all the events, I’d go back and extract some information from my array. Except of course even after you’re done dealing with the object, there’s no guarantee that the array isn’t still being written to (because it’s happening asynchronously in the background). It also appears to me after testing that even if you Unregister-Event it doesn’t guarantee that it might not be finished writing to the array in the background.

    I would like to override it with the add_ function to process synchronously and thus solve my problem, but that function doesn’t give you a way to include your own MessageData objects, and unfortunately using global objects isn’t going to be safe for this because my code has a lot running asynchronously in other threads – I wouldn’t want to clobber stuff.

    I haven’t found a way to simulate add_ with MessageData in PowerShell because in C# it involves a lot of delegate trickery under the covers šŸ˜¦

    Comment by Cody — February 3, 2017 @ 7:06 am

    • Hi Cody,
      Thanks for reporting back. I’m trying to solve this with scriptblocks, but I can’t seem to get the hang of variable scoping in PowerShell. I expected this to be more closure-like but I can’t quite get it working. I’ve posted a question on StackOverflow that might get us a little further.

      Update: It seems it is possible, but you have to create the closure explicitly:
      $handler = {
      param($MessageData)
      {
      param($Source, $EventArgs)
      if ($EventArgs.Message -eq $MessageData) {
      $EventArgs.Skip = $true
      } else {
      $EventArgs.Message = ($EventArgs.Message + ".00")
      }
      }.GetNewClosure()
      }

      Then, when adding the handler to the object, you can specify MessageData:
      $echoObject.add_BeforeEcho((& $handler -MessageData "2"))
      $echoObject.add_BeforeEcho((& $handler -MessageData "3"))
      $echoObject.EchoMessages('1,2,3,4,5')

      This should now output:
      1
      4
      5

      Of course, you could also have multiple different handlers, each with their own MessageData. And if you don’t want do go with the closure solution, you could try inlining the MessageData with each different handler. Harder if it’s a complex object, but possibly easier when just a simple string or number.

      Comment by wensveen — February 3, 2017 @ 10:38 am


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: