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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With