Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# events never handled in PowerShell 5

I'm trying to create a PowerShell add-in in .NET which will allow me to access some Windows 10 functionality. I know C#, but PowerShell is relatively new to me.

I have all my function calls and cmdlets working. I can make the C# classes do things from PowerShell. What's tripping me up is event raising and handling to let the C# code call back. I've been on this for three evenings now.

Examples online and here in SO always show timers and file system watchers, or sometimes a Windows Form. I have never seen an example using a class someone has written themselves. Not sure if there's some sugar required in raising the events, or something else.

The main difference I've seen between my code and others is that I have a factory class that returns an instance of the object which raises the events. I use that instead of calling new-object. However, PowerShell recognizes the type and is happy to list members on it, so I have assumed that is not the problem.

I've created a simple repro cmdlet and class, and scripts. Here they are:

GetTestObject cmdlet

using System.Management.Automation;   

namespace PeteBrown.TestFoo
{
    [Cmdlet(VerbsCommon.Get, "TestObject")]

    public class GetTestObject : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            var test = new TestObject();
            WriteObject(test);
        }
    }
}

TestObject class

using System;

namespace PeteBrown.TestFoo
{
    public class TestObject
    {
        public string Name { get; set; }

        public event EventHandler FooEvent;

        public void CauseFoo()
        {
            Console.WriteLine("c#: About to raise foo event.");
            try
            {
                if (FooEvent != null)
                    FooEvent(this, EventArgs.Empty);
                else
                    Console.WriteLine("c#: no handlers wired up.");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            Console.WriteLine("c#: Raised foo event. Should be handler executed above this line.");
        }


        public void CauseAsyncFoo()
        {
            Console.WriteLine("c#: About to raise async foo event.");
            try
            {
                if (FooEvent != null)
                {
                    // yeah, I know this is silly
                    var result = FooEvent.BeginInvoke(this, EventArgs.Empty, null, null);
                    FooEvent.EndInvoke(result);
                }
                else
                    Console.WriteLine("c#: no handlers wired up.");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            Console.WriteLine("c#: Raised async foo event.");
        }

    }
}

And here are the scripts and their output

test-events1.ps1 using simple .NET event handler syntax

Import-Module "D:\U.....\Foo.dll"

Write-Output "Getting test object -------------------------------------- "
[PeteBrown.TestFoo.TestObject]$obj = Get-TestObject

# register for the event
Write-Output "Registering for .net object event ------------------------ "
$obj.add_FooEvent({Write-Output "Powershell: Event received"}) 

Write-Output "Calling the CauseFoo method to raise event --------------- "
$obj.CauseFoo()

Output: (event is fired in C#, but never handled in PS)

Getting test object --------------------------------------
Registering for .net object event ------------------------
Calling the CauseFoo method to raise event ---------------
c#: About to raise foo event.
c#: Raised foo event. Should be handler executed above this line.

test-events2.ps1 using async / BeginInvoke syntax in case that was the issue

Import-Module "D:\U.....\Foo.dll"

Write-Output "Getting test object -------------------------------------- "
[PeteBrown.TestFoo.TestObject]$obj = Get-TestObject

# register for the event
Write-Output "Registering for .net object event ------------------------ "
$obj.add_FooEvent({Write-Output "Powershell: Event received"}) 

Write-Output "Calling the CauseAsyncFoo method to raise event ---------- "
$obj.CauseAsyncFoo()

Output: (event is fired in C#, but never handled in PS. Also, I get an exception.)

Getting test object --------------------------------------
Registering for .net object event ------------------------
Calling the CauseAsyncFoo method to raise event ----------
c#: About to raise async foo event.
System.ArgumentException: The object must be a runtime Reflection object.
   at System.Runtime.Remoting.InternalRemotingServices.GetReflectionCachedData(MethodBase mi)
   at System.Runtime.Remoting.Messaging.Message.UpdateNames()
   at System.Runtime.Remoting.Messaging.Message.get_MethodName()
   at System.Runtime.Remoting.Messaging.MethodCall..ctor(IMessage msg)
   at System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(Object NotUsed, MessageData& msgData)
   at System.EventHandler.BeginInvoke(Object sender, EventArgs e, AsyncCallback callback, Object object)
   at PeteBrown.TestFoo.TestObject.CauseAsyncFoo() in D:\Users\Pete\Documents\GitHub\Windows-10-PowerShell-MIDI\PeteBrow
n.PowerShellMidi\Test\TestObject.cs:line 37
c#: Raised async foo event.

test-events3.ps1 Using Register-ObjectEvent approach. Added some more diagnostics type info to the output here as well.

Import-Module "D:\U......\Foo.dll"

Write-Output "Getting test object -------------------------------- "
[PeteBrown.TestFoo.TestObject]$obj = Get-TestObject

# register for the event
Write-Output "Registering for .net object event ------------------ "

$job = Register-ObjectEvent -InputObject $obj -EventName FooEvent -Action { Write-Output "Powershell: Event received" }

Write-Output "Calling the CauseFoo method to raise event --------- "
$obj.CauseFoo()

Write-Output "Job that was created for the event subscription ----"
Write-Output $job

# show the event subscribers
Write-Output "Current event subscribers for this session ---------"
Get-EventSubscriber

# this lists the events available
Write-Output "Events available for TestObject --------------------"
$obj | Get-Member -Type Event

Output: (event is fired in C#, but never handled in PS)

Getting test object --------------------------------
Registering for .net object event ------------------
Calling the CauseFoo method to raise event ---------
c#: About to raise foo event.
c#: Raised foo event. Should be handler executed above this line.
Job that was created for the event subscription ----

Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      2626de85-523...                 Running       True                                  Write-Output "Powersh...
Current event subscribers for this session ---------

SubscriptionId   : 1
SourceObject     : PeteBrown.TestFoo.TestObject
EventName        : FooEvent
SourceIdentifier : 2626de85-5231-44a0-8f2c-c2a900a4433b
Action           : System.Management.Automation.PSEventJob
HandlerDelegate  :
SupportEvent     : False
ForwardEvent     : False

Events available for TestObject --------------------

TypeName   : PeteBrown.TestFoo.TestObject
Name       : FooEvent
MemberType : Event
Definition : System.EventHandler FooEvent(System.Object, System.EventArgs)

In no cases do I actually get the event handler action fired off. I've also tried using a variable to hold the action, but that seems to be treated the same way. The Console.Writeline code in C# is just to help debug. And I removed the full path for the DLL in the pasted in scripts just to make them easier to read. The DLL loads fine.

Using PowerShell 5 on Windows 10 pro 64 bit with .NET Framework 4.6 (CLR 4). VS 2015 RTM.

Name                           Value
----                           -----
PSVersion                      5.0.10240.16384
WSManStackVersion              3.0
SerializationVersion           1.1.0.1
CLRVersion                     4.0.30319.42000
BuildVersion                   10.0.10240.16384
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3

Any suggestions for this PowerShell noob?

like image 824
Pete Avatar asked Aug 12 '15 04:08

Pete


1 Answers

Write-Output does not display object on console, it send object to the next command in pipeline. If you want immediately display some text on console, then you have to use Write-Host or Out-Host cmdlet.

"Never use Write-Host because evil!"

I would say it this way: "Never use Write-Host on intermediate data because evil!". It is OK to display some progress notification with Write-Host (although you can consider of using Write-Progress instead) or use it to display final result, but Write-Host send nothing to next command in pipeline, so you can not process data any further.

Why do all the other Write-Output commands write to the console?

When some object reach ultimate end of pipeline, PowerShell have to do something about it. By default it display it on host (console in this case), although you can override this:

New-Alias Out-Default Set-Clipboard # to copy anything into clipboard
New-Alias Out-Default Out-GridView  # to use grid view by default

PowerShell does not support of script block invocation in some arbitrary thread, thus for asynchronous events you have to use Register-ObjectEvent, so PowerShell can handle them.

like image 192
user4003407 Avatar answered Sep 30 '22 13:09

user4003407