Here is the basic gist of my problem:
The magic of .NET events hides the fact that, when you subscribe to an event in an instance of B by an instance of A, A gets sent over into B's appdomain. If A isn't MarshalByRef, then a value-copy of A is sent. Now you've got two separate instances of A, which is why you experienced the unexpected behaviors.
If anyone is having a hard time understanding how this happens, I suggest the following workaround which makes it obvious why events behave this way.
In order to raise "events" in B (within appdomain 2) and handle them in A (within appdomain 1) without using real events, we'll need to create a second object which translates method calls (which cross boundaries without much ado) to events (which don't behave how you might expect). This class, lets call it X, will be instantiated in appdomain 1, and its proxy will be sent into appdomain 2. Here's the code:
public class X : MarshalByRefObject
{
public event EventHandler MyEvent;
public void FireEvent(){ MyEvent(this, EventArgs.Empty); }
}
The pseudocode would go something like:
In order for B to fire an event back in AD1, it not only must have the method but also an instance to fire that method on. That's why we have to send a proxy of X into AD2. This is also why cross-domain events require the event handler to be marshalled across the domain boundary! An event is just a fancy wrapper around a method execution. And to do that you need not only the method but also the instance to execute it on.
The rule of thumb must be that if you wish to handle events across an application domain boundary, both types--the one exposing the event and the one handling it--must extend MarshalByRefObject.
In my first attempt at solving this issue, I removed Class B's inheritance of MarshalByRefObject
and flagged it as serializable instead. The result was the the object was marshaled by value and I just got a copy of Class C that executes in the host AppDomain. This is not what I wanted.
The real solution, I found, was that Class B (DangerousProgram
in the example) should also inherit from MarshalByRefObject
so that the call back also uses a proxy to transition the thread back to the default AppDomain.
By the way, here's a great article I found by Eric Lippert that explains marshal by ref vs. marshal by value in a very clever way.