Search code examples
.neteventsremoting

How can I subscribe to an event across AppDomains (object.Event += handler;)


I'm having the problem described in this message board post.

I have an object that is hosted in its own AppDomain.

public class MyObject : MarshalByRefObject
{
    public event EventHandler TheEvent;
    ...
    ...
}

I'd like to add a handler to that event. The handler will run in a different AppDomain. My understanding is this is all good, events get delivered across that boundary magically, with .NET Remoting.

But, when I do this:

// instance is an instance of an object that runs in a separate AppDomain
instance.TheEvent += this.Handler ; 

...it compiles fine but fails at runtime with:

System.Runtime.Remoting.RemotingException: 
     Remoting cannot find field 'TheEvent' on type 'MyObject'.

Why?

EDIT: source code of working app that demonstrates the problem:

// EventAcrossAppDomain.cs
// ------------------------------------------------------------------
//
// demonstrate an exception that occurs when trying to use events across AppDomains.
//
// The exception is:
// System.Runtime.Remoting.RemotingException:
//       Remoting cannot find field 'TimerExpired' on type 'Cheeso.Tests.EventAcrossAppDomain.MyObject'.
//
// compile with:
//      c:\.net3.5\csc.exe /t:exe /debug:full /out:EventAcrossAppDomain.exe EventAcrossAppDomain.cs
//

using System;
using System.Threading;
using System.Reflection;

namespace Cheeso.Tests.EventAcrossAppDomain
{
    public class MyObject : MarshalByRefObject
    {
        public event EventHandler TimerExpired;
        public EventHandler TimerExpired2;

        public  MyObject() { }

        public void Go(int seconds)
        {
            _timeToSleep = seconds;
            ThreadPool.QueueUserWorkItem(Delay);
        }

        private void Delay(Object stateInfo)
        {
            System.Threading.Thread.Sleep(_timeToSleep * 1000);
            OnExpiration();
        }

        private void OnExpiration()
        {
            Console.WriteLine("OnExpiration (threadid={0})",
                              Thread.CurrentThread.ManagedThreadId);
            if (TimerExpired!=null)
                TimerExpired(this, EventArgs.Empty);

            if (TimerExpired2!=null)
                TimerExpired2(this, EventArgs.Empty);
        }

        private void ChildObjectTimerExpired(Object source, System.EventArgs e)
        {
            Console.WriteLine("ChildObjectTimerExpired (threadid={0})",
                              Thread.CurrentThread.ManagedThreadId);
            _foreignObjectTimerExpired.Set();
        }

        public void Run(bool demonstrateProblem)
        {
            try 
            {
                Console.WriteLine("\nRun()...({0})",
                                  (demonstrateProblem)
                                  ? "will demonstrate the problem"
                                  : "will avoid the problem");

                int delaySeconds = 4;
                AppDomain appDomain = AppDomain.CreateDomain("appDomain2");
                string exeAssembly = Assembly.GetEntryAssembly().FullName;

                MyObject o = (MyObject) appDomain.CreateInstanceAndUnwrap(exeAssembly,
                                                                          typeof(MyObject).FullName);

                if (demonstrateProblem)
                {
                    // the exception occurs HERE
                    o.TimerExpired += ChildObjectTimerExpired;
                }
                else
                {
                    // workaround: don't use an event
                    o.TimerExpired2 = ChildObjectTimerExpired;
                }

                _foreignObjectTimerExpired = new ManualResetEvent(false);

                o.Go(delaySeconds);

                Console.WriteLine("Run(): hosted object will Wait {0} seconds...(threadid={1})",
                                  delaySeconds,
                                  Thread.CurrentThread.ManagedThreadId);

                _foreignObjectTimerExpired.WaitOne();

                Console.WriteLine("Run(): Done.");

            }
            catch (System.Exception exc1)
            {
                Console.WriteLine("In Run(),\n{0}", exc1.ToString());
            }
        }



        public static void Main(string[] args)
        {
            try 
            {
                var o = new MyObject();
                o.Run(true);
                o.Run(false);
            }
            catch (System.Exception exc1)
            {
                Console.WriteLine("In Main(),\n{0}", exc1.ToString());
            }
        }

        // private fields
        private int _timeToSleep;
        private ManualResetEvent _foreignObjectTimerExpired;

    }
}

Solution

  • The reason that your code example fails is that the event declaration and the code that subscribes to it is in the same class.

    In this case, the compiler "optimizes" the code by making the code that subscribes to the event access the underlying field directly.

    Basically, instead of doing this (as any code outside of the class will have to):

    o.add_Event(delegateInstance);
    

    it does this:

    o.EventField = (DelegateType)Delegate.Combine(o.EventField, delegateInstance);
    

    so, the question I have for you is this: Does your real example have the same layout of code? Is the code that subscribes to the event in the same class that declares the event?

    If yes, then the next question is: Does it have to be there, or should it really be moved out of it? By moving the code out of the class, you make the compiler use the add and ? remove special methods that are added to your object.

    The other way, if you cannot or won't move the code, would be to take over responsibility for adding and removing delegates to your event:

    private EventHandler _TimerExpired;
    public event EventHandler TimerExpired
    {
        add
        {
            _TimerExpired += value;
        }
    
        remove
        {
            _TimerExpired -= value;
        }
    }
    

    This forces the compiler to call the add and remove even from code inside the same class.