RickStrahl / wwDotnetBridge

.NET Interop for Visual FoxPro made easy
http://west-wind.com/wwDotnetBridge.aspx
MIT License
74 stars 35 forks source link

Event subscriptions #12

Closed breyed closed 6 years ago

breyed commented 6 years ago

Mechanism for subscribing to .NET events without COM registration.

breyed commented 6 years ago

This is an attempt to fill the gap in #11. We've tested the .NET code and plan to test the FoxPro side tomorrow. In the meantime, we may try to wrap the async call to WaitForEvent. In this usage, AsyncCallbackEvents is more heavy-weight than needed. OnError is never called. OnCompleted would be better replaced by a function OnEvent(lcEventName,loParameters).

breyed commented 6 years ago

I added a wrapper to call OnEvent. It could be made more elegant if it used dynamic code to call a FoxPro event named according to single .NET event subscribed to, with just its event parameter, rather than the current approach of subscribing to all events.

Additionally, there appears to be a problem with object lifetimes, which is difficult to debug, perhaps due to #13.

breyed commented 6 years ago

We modified the callback API to provide dispatch customized to the event and removed the need for the end user to call WaitForEvent. It's pretty cool - and works too! We haven't tested subscription lifetime yet. We plan to do so tomorrow.

GetIndexedProperty didn't work to get the raised event parameters from lvResult.Params. It always accessed the first array element. So we used an hack workaround instead.

RickStrahl commented 6 years ago

This all looks very nice - I'll take a look when I get a chance.

RickStrahl commented 6 years ago

I Edward,

I pulled this down and merged in the latest changes from master.

I'm trying to get my head around how this is supposed to work, but I'm not having any luck. Say I want to capture events from FileSystemWatcher. I can't see how would set up the events to handle with the EventSubscriber and channel that all the way back to Foxpro.

Something like this?

var fw = new FileSystemWatcher();
var ev = new EventSubscriber(fw);

var result = ev.WaitForEvent();   // Keep this alive in a loop after event?

Then how do I get this back to FoxPro? Pass in an object that implements methods and try to call the method in the event with a T/C handler around it to guard against missing or invalid signatures? How does this thing stay alive then?

I guess I'm not sure how this is supposed work. It would help if you can set up an example perhaps that calls this through all the way to the client that's listening.

breyed commented 6 years ago

The ReadMe Events section has an example of how this works in FoxPro. It uses SmtpClient, but is otherwise analogous to how it would look for FileSystemWatcher. If we add support for handling only certain events (which I think we should), FileSystemWatcher would be a better candidate for the readme example, since it has more than one event. An example that monitors file creation would look like this:

LOCAL loFsw, loFswHandler, loFswEventSubscription
loFsw = loBridge.CreateInstance("System.IO.FileSystemWatcher")
loFswHandler = CREATEOBJECT("MyFswEventHandler")
loFswEventSubscription = loBridge.SubscribeToEvents(loFsw, loFswHandler)
* Manipulate file system here

DEFINE CLASS MyFswEventHandler as Custom
PROCEDURE Created(loSender, loEventArgs)
* Handle new file creation here
ENDPROC
PROCEDURE Error(loSender, loEventArgs)
* Handle file system watcher error
ENDPROC
ENDDEFINE

Note that from FoxPro, there is no need for the user of wwDotNetBridge to call WaitForEvent. The FoxPro class EventSubscription repeatedly calls WaitForEvent automatically.

EventSubscriberTests.cs contains an example of how the C# code works in isolation, including a call to WaitForEvent. However, aside from the unit test, WaitForEvent is never called from C#. The FoxPro code gets event parameters by calling WaitForEvent directly.

To document how the C# and FoxPro code interact, I added remarks to EventSubscriber:

For a FoxPro program to be notified of events, it should use wwDotNetBridge.InvokeMethodAsync to call WaitForEvent. When WaitForEvent asynchronously completes, the FoxPro program should handle the event it returns and then call WaitForEvent again to wait for the next event. The FoxPro class EventSubscription, which is returned by SubscribeToEvents, encapsulates this async wait loop.

Currently, there is no guard against missing signatures in the user's event handler. This is bad, because I think the exception would propagate back into whatever context the async invoke was piggy-backing on. I think we should add missing signature and event handler exception handling in coordination with #13.

RickStrahl commented 6 years ago

Ok so I set this up with an example for the File System Watcher, but I don't see any events firing.

Never mind I figured it out - it is working with this code:


do wwDotNetBridge
LOCAL loBridge as wwDotNetBridge
loBridge = CreateObject("wwDotNetBridge","V4")

loFW = loBridge.CreateInstance("System.IO.FileSystemWatcher","C:\temp")
loFw.EnableRaisingEvents = .T.

loFwHandler = CREATEOBJECT("FwEventHandler")
loSubscription = loBridge.SubscribeToEvents(loFw, loFwHandler)

DOEVENTS

lcFile = "c:\temp\test.txt"
DELETE FILE ( lcFile )  
STRTOFILE("DDD",lcFile)
STRTOFILE("FFF",lcFile)

WAIT WINDOW

RETURN

DEFINE CLASS FwEventHandler as Custom

FUNCTION OnCreated(sender,ev)
? "FILE CREATED: "
?  ev.FullPath
ENDFUNC

FUNCTION OnChanged(sender,ev)
? "FILE CHANGE: "
?  ev.FullPath
ENDFUNC

FUNCTION OnDeleted(sender, ev)
? "FILE DELETED: "
?  ev.FullPath
ENDFUNC

FUNCTION OnRenamed(sender, ev)
LOCAL lcOldPath, lcPath

? "FILE RENAMED: " 
loBridge = GetwwDotnetBridge()

*** Not sure why these require indirect referencing but they do
lcOldPath = loBridge.GetProperty(ev,"OldFullPath")
lcPath = loBridge.GetProperty(ev,"FullPath")
? lcOldPath + " -> " + lcPath

ENDFUNC

ENDDEFINE

As to #13 I added exception handlers around the InvokeMethodAsync callbacks so if the handlers aren't there the exceptions at least won't crash VFP, but there is no feedback beyond the LastException getting set (which is of course not all that useful in Async scenarios).

I made those changes on Master and merged them into this branch, so you should see them here, although I'm not sure if this is going through right. The merge conflict that's showing here shouldn't be showing since master is merged and StringUtils of all things is a weird thing to show a conflict.

breyed commented 6 years ago

Between jumping from the GitHub email to the web page for this issue, I missed the strikethrough on your first line. So when I copied your code from this issue web page and it didn't work, I wasn't surprised. To help debug, I added EventTests.prg as a member of the Tests project. It first tests with a simple Loopback class that simply raises an event. Then it tests with FileSystemWatcher. I did get the FileSystemWatcher test to work, but only after I added SET SAFETY OFF. It seems that the warning dialog interfered with how InvokeMethodAsync's completion calls were getting injected into FoxPro.

We've tested events with a complex production application, and they're working perfectly, so I wasn't expecting any problems. The only remaining issue was lifetime support. There was a bug with the code in EventSubscription to unsubscribe when it went out of scope. The problem was that the async method held a reference to it, so even when the code that called SubscribeToEvents let the object it returned go out of scope, it still had a reference.

To fix the problem, I made subscription control explicit. EventSubscription now has an Unsubscribe method the user calls when done.

One other feature that might be nice would be for SubscribeToEvents to take an optional prefix parameter. When supplied, it would be used instead of On. That would allow subscribing to events from different classes without name conflicts. [Update: I implemented this.]

Regarding the flow in GitHub, the recommended approach is to do all the editing in the PR issuer's repo. Once the update is stabilized, merge it into the upstream master branch. In our case, that would mean you deleting your breyed-events branch, us both making needed edits to my events branch, and when you think it's ready, you merging my events branch into your master branch, building and committing the release DLL, and then pushing your master to GitHub.

RickStrahl commented 6 years ago

Not sure why this branch is not showing as merged as I've merged changes into master on my end and I see your merged content.

Closing.