views:

636

answers:

2

Here is the basic gist of my problem:

  1. My main Window class instantiates Class A.
  2. Class A instantiates Class B in a secondary AppDomain.
  3. Class B raises an event and Class A handles the event successfully.
  4. Class A raises an event of its own.

Problem: In step 4, when Class A raises its own event from the event handler method that caught Class B's event, the event is raised; however, the subscribing handler in the Window class is never called.

There are no exceptions being thrown. If I remove the secondary AppDomain, the event gets handled without a problem.

Does anyone know why this doesn't work? Is there another way to make this work without using a callback?

I would think, if anything, the problem would occur in step 3 instead of step 4.

Here's a real code sample to illustrate the problem:

Class Window1

    Private WithEvents _prog As DangerousProgram    

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click    
        _prog = New DangerousProgram()
        _prog.Name = "Bad Program"  
    End Sub

    Private Sub MyEventHandler(ByVal sender As Object, ByVal e As NameChangedEventArgs) Handles _prog.NameChanged
        TextBox1.Text = "Program's name is now: " & e.Name
    End Sub

End Class


<Serializable()> _    
Public Class DangerousProgram

    Private _appDomain As AppDomain
    Private WithEvents _dangerousProgram As Program
    Public Event NameChanged(ByVal sender As Object, ByVal e As NameChangedEventArgs)


    Public Sub New()

        // DangerousPrograms are created inside their own AppDomain for security.

        _appDomain = AppDomain.CreateDomain("AppDomain")    
        Dim assembly As String = System.Reflection.Assembly.GetEntryAssembly().FullName 
        _dangerousProgram = CType( _   
                    _appDomain.CreateInstanceAndUnwrap(assembly, _    
                        GetType(Program).FullName), Program)

    End Sub


    Public Property Name() As String
        Get
            Return _dangerousProgram.Name
        End Get
        Set(ByVal value As String)
            _dangerousProgram.Name = value
        End Set
    End Property


    Public Sub NameChangedHandler(ByVal sender As Object, ByVal e As NameChangedEventArgs) Handles _dangerousProgram.NameChanged    
        Debug.WriteLine(String.Format("Caught event in DangerousProgram. Program name is {0}.", e.Name))
        Debug.WriteLine("Re-raising event...")

        RaiseEvent NameChanged(Me, New NameChangedEventArgs(e.Name))   
    End Sub

End Class


<Serializable()> _    
Public Class Program
    Inherits MarshalByRefObject

    Private _name As String
    Public Event NameChanged(ByVal sender As Object, ByVal e As NameChangedEventArgs)

    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
            RaiseEvent NameChanged(Me, New NameChangedEventArgs(_name))
        End Set
    End Property   

End Class


<Serializable()> _   
Public Class NameChangedEventArgs
    Inherits EventArgs

    Public Name As String

    Public Sub New(ByVal newName As String)
        Name = newName
    End Sub

End Class
+1  A: 

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.

Rob Sobers
+4  A: 

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:

  1. A, within AD1, creates a new appdomain. Call it AD2.
  2. A calls CreateInstanceAndUnwrap on AD2. B now exists in AD2 and B(proxy) exists in AD1.
  3. A creates an instance of X.
  4. A hands X to B(proxy)
  5. In AD2, B now has an instance of X(proxy) (X is MBRO)
  6. In AD1, A registers an event handler with X.MyEvent
  7. In AD2, B calls X(proxy).FireEvent()
  8. In AD1, FireEvent executes on X, which fires MyEvent
  9. A's event handler for FireEvent executes.

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.

Will
I like your suggestion. Your last statement is an excellent summary. Thanks!
Rob Sobers