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

3

15

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;

    }
}
Cran answered 7/9, 2009 at 18:46 Comment(0)
L
13

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.

Lindsylindy answered 10/10, 2009 at 18:4 Comment(0)
A
6

Events work fine in remoting, but there are some complications, and I'm guessing you're running into one of them.

The main issue is that, for a client to subscribe to a remoted server object's event, the framework needs to have type information for both the client and the server available on both ends. Without this, you can get some remoting exceptions similar to what you're seeing.

There are ways around this, including using the observer pattern manually (vs. using an event directly), or providing a base class or interface that's available on both sides of the wire.

I recommend reading this CodeProject article. It walks through using events with remoting, and has a good description of this issue, in the section titled "Raising events from remote objects".

Basically, the main thing is to make sure your handlers follow specific guidelines, including being concrete, non-virtual, etc. The article walks through specifics, and provides working examples.

Aborigine answered 7/9, 2009 at 19:13 Comment(5)
ok, but does the "type information on both ends" issue apply if the type with the event is defined in the same assembly as the type with the handler, and if te two AppDomains are running within the same process on the same machine? It is an ASPNET custom host. The program starts up and calls CreateApplicationHost().Cran
I also tried it using the same Type as both the publisher and subscriber of the event. One instance of the type is the publisher, another instance of the type in a separate AppDomain is the subscriber. Same results. So it seems like the "type info is not available on both ends of the wire" is not the issue I am seeing.Cran
It should work if they're the same type of object. Are you subscribing to a public, non virtual method (ie: the handler)? If the method's virutal, it often causes strange issues.Aborigine
yes, public, non virtual. I will post full source of an example that reproduces the problem.Cran
OK, the example that demonstrates the problem is up.Cran
R
0

An application domain is an isolated environment where applications execute. In other words, it's a partition in an operating system process where one or more applications reside.

  1. AppDomain allows us to load DLL at runtime.
  2. For communication between 'AppDomain' boundary, the types should be Serializable.
  3. Derive from class MarshalByRefObject which enables access to objects across application domain boundaries in applications that support remoting.
  4. Full name of DLL assembly is used to load it into AppDomain. For now, we place it in the same folder as the main program.

In this section, we detailed how to achieve sending and receiving events through Application Domain boundary. Here we are using the shared universal library with interfaces already known to us, and two separate publisher and subscriber DLLs loaded on runtime and fire events across domains.

For understanding, we use four separate projects.

  1. EventsCommon (Class Library Project) It defines standard interfaces for Publisher and Subscriber classes, and the Main Class uses it to create interface objects.

        namespace EventCommons
        {
            using System;
    
            /// <summary>
            /// Common Interface for Publisher
            /// </summary>
            public interface IEventCommonGenerator
            {
                /// <summary>
                /// Name Generator with <see cref="Action{T}"/> accepts string and return void
                /// </summary>
                event Action<string> NameGenerator;
    
                /// <summary>
                /// Fire Events
                /// </summary>
                /// <param name="input"></param>
                void FireEvent(string input);
            }
    
            /// <summary>
            /// Common Interface for Subscriber
            /// </summary>
            public interface IEventCommonCatcher
            {
                /// <summary>
                /// Print Events executed
                /// </summary>
                /// <returns></returns>
                string PrintEvents();
    
                /// <summary>
                /// Subscribe to Publisher's <see cref="IEventCommonGenerator.NameGenerator"/> event
                /// </summary>
                /// <param name="commonGenerator"></param>
                void Subscribe(IEventCommonGenerator commonGenerator);
            }
        }
    
    1. EventsPublisher (Class Library Project) It references EventCommon project and implements Publisher related Interface IEventCommonGenerator from EventCommon.

      namespace EventsPublisher
      {
          using EventCommons;
          using System;
      
          /// <summary>
          /// Implements <see cref="IEventCommonGenerator"/> from <see cref="EventCommons"/>
          /// </summary>
          [Serializable]
          public class EventsGenerators : IEventCommonGenerator
          {
              /// <summary>
              /// Fires Event
              /// </summary>
              /// <param name="input"></param>
              public void FireEvent(string input)
              {
                  this.NameGenerator?.Invoke(input);
              }
      
              /// <summary>
              /// Event for Publisher
              /// </summary>
              public event Action<string> NameGenerator;
          }
      }
      
    2. EventsSubscriber (Class Library Project) It references EventCommon project and implements Subscriber-related Interface IEventCommonCatcher from EventCommon.

      namespace EventsSubscriber
      {
          using System;
          using System.Collections.Generic;
          using EventCommons;
      
          /// <summary>
          /// Implements <see cref="IEventCommonCatcher"/> from <see cref="EventCommons"/>
          /// </summary>
          [Serializable]
          public class EventsCatcher : IEventCommonCatcher
          {
              /// <summary>
              /// Initializes object of <see cref="ReceivedValueList"/> and <see cref="EventsCatcher"/>
              /// </summary>
              public EventsCatcher()
              {
                  this.ReceivedValueList = new List<string>();
              }
      
              /// <summary>
              /// Subscribes to the Publisher
              /// </summary>
              /// <param name="commonGenerator"></param>
              public void Subscribe(IEventCommonGenerator commonGenerator)
              {
                  if (commonGenerator != null)
                  {
                      commonGenerator.NameGenerator += this.CommonNameGenerator;
                  }
              }
      
              /// <summary>
              /// Called when event fired from <see cref="IEventCommonGenerator"/> using <see cref="IEventCommonGenerator.FireEvent"/>
              /// </summary>
              /// <param name="input"></param>
              private void CommonNameGenerator(string input)
              {
                  this.ReceivedValueList.Add(input);
              }
      
              /// <summary>
              /// Holds Events Values
              /// </summary>
              public List<string> ReceivedValueList { get; set; }
      
              /// <summary>
              /// Returns Comma Separated Events Value
              /// </summary>
              /// <returns></returns>
              public string PrintEvents()
              {
                  return string.Join(",", this.ReceivedValueList);
              }
          }
      }
      
    3. CrossDomainEvents (Main Console Application) It loads EventsPublisher into Publisher AppDomain and EventsSubscriber into Subscriber AppDomain, Subscribes Events of Publisher AppDomain into Subscriber AppDomain and Fires the event.

      using System;
      
      namespace CrossDomainEvents
      {
          using EventCommons;
      
          class Program
          {
              static void Main()
              {
                  // Load Publisher DLL
                  PublisherAppDomain.SetupDomain();
                  PublisherAppDomain.CustomDomain.Load("EventsPublisher, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
                  var newPublisherGenerator = PublisherAppDomain.Instance as IEventCommonGenerator;
      
                  // Load Subscriber DLL
                  SubscriberAppDomain.SetupDomain(newPublisherGenerator);
                  SubscriberAppDomain.CustomDomain.Load("EventsSubscriber, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
                  var newSubscriberCatcher = SubscriberAppDomain.Instance as IEventCommonCatcher;
      
                  // Fire Event from Publisher and validate event on Subscriber
                  if (newSubscriberCatcher != null && newPublisherGenerator != null)
                  {
                      // Subscribe Across Domains
                      newSubscriberCatcher.Subscribe(newPublisherGenerator);
      
                      // Fire Event
                      newPublisherGenerator.FireEvent("First");
      
                      // Validate Events
                      Console.WriteLine(newSubscriberCatcher.PrintEvents());
                  }
      
                  Console.ReadLine();
              }
          }
      
          /// <summary>
          /// Creates Publisher AppDomain
          /// </summary>
          public class PublisherAppDomain : MarshalByRefObject
          {
      
              public static AppDomain CustomDomain;
              public static object Instance;
      
              public static void SetupDomain()
              {
                  // Domain Name EventsGenerator
                  CustomDomain = AppDomain.CreateDomain("EventsGenerator");
                  // Loads EventsPublisher Assembly and create EventsPublisher.EventsGenerators
                  Instance = Activator.CreateInstance(CustomDomain, "EventsPublisher, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "EventsPublisher.EventsGenerators").Unwrap();
              }
          }
      
          /// <summary>
          /// Creates Subscriber AppDomain
          /// </summary>
          public class SubscriberAppDomain : MarshalByRefObject
          {
      
              public static AppDomain CustomDomain;
              public static object Instance;
      
              public static void SetupDomain(IEventCommonGenerator eventCommonGenerator)
              {
                  // Domain Name EventsCatcher
                  CustomDomain = AppDomain.CreateDomain("EventsCatcher");
                  // Loads EventsSubscriber Assembly and create EventsSubscriber.EventsCatcher
                  Instance = Activator.CreateInstance(
                      CustomDomain,
                      "EventsSubscriber, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
                      "EventsSubscriber.EventsCatcher").Unwrap();
              }
          }
      
      }
      

Note: We need to ensure that the EventsSubscriber.dll and EventsPublisher.dll are present in the same folder as CrossDomainEvents.exe. It can be done using XCOPY command in Publisher and Subscriber projects to paste DLL in CrossDomainEvents Project Output Directory.

Rivas answered 29/5, 2018 at 20:59 Comment(1)
I have written one article for events across AppDomain. Try if you have time. blog.vcillusion.co.in/… Hope it helps!Rivas

© 2022 - 2024 — McMap. All rights reserved.