如何跨 AppDomains 订阅事件(object.Event += handler;)

Posted

技术标签:

【中文标题】如何跨 AppDomains 订阅事件(object.Event += handler;)【英文标题】:How can I subscribe to an event across AppDomains (object.Event += handler;) 【发布时间】:2010-11-26 07:24:44 【问题描述】:

我遇到了this message board post 中描述的问题。

我有一个托管在它自己的 AppDomain 中的对象。

public class MyObject : MarshalByRefObject

    public event EventHandler TheEvent;
    ...
    ...

我想为该事件添加一个处理程序。处理程序将在不同的 AppDomain 中运行。我的理解是,这一切都很好,事件通过 .NET Remoting 神奇地跨越该边界传递。

但是,当我这样做时:

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

...它编译正常,但在运行时失败:

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

为什么?

编辑:演示问题的工作应用的源代码:

// 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(),\n0", 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(),\n0", exc1.ToString());
            
        

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

    

【问题讨论】:

【参考方案1】:

应用程序域是应用程序执行的隔离环境。换句话说,它是操作系统进程中的一个分区,一个或多个应用程序驻留在其中。

    AppDomain 允许我们在运行时加载 DLL。 对于“AppDomain”边界之间的通信,类型应该是可序列化的。 派生自 MarshalByRefObject 类,该类允许在支持远程处理的应用程序中跨应用程序域边界访问对象。 DLL 程序集的全名用于将其加载到 AppDomain。现在,我们将它放在与主程序相同的文件夹中。

在本节中,我们详细介绍了如何通过应用程序域边界来实现发送和接收事件。在这里,我们使用共享通用库和我们已知的接口,以及在运行时加载的两个单独的发布者和订阅者 DLL,并跨域触发事件。

为了便于理解,我们使用了四个独立的项目。

    EventsCommon(类库项目) 它为发布者和订阅者类定义了标准接口,主类使用它来创建接口对象。

        namespace EventCommons
        
            using System;
    
            /// <summary>
            /// Common Interface for Publisher
            /// </summary>
            public interface IEventCommonGenerator
            
                /// <summary>
                /// Name Generator with <see cref="ActionT"/> 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);
            
        
    

      EventsPublisher(类库项目) 引用EventCommon项目,从EventCommon实现Publisher相关接口IEventCommonGenerator。

      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;
          
      
      

      EventsSubscriber(类库项目) 它引用了EventCommon项目,并从EventCommon中实现了订阅者相关的接口IEventCommonCatcher。

      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);
              
          
      
      

      CrossDomainEvents(主控制台应用程序) 它将 EventsPublisher 加载到发布者 AppDomain 中,将 EventsSubscriber 加载到订阅者 AppDomain 中,将发布者 AppDomain 的事件订阅到订阅者 AppDomain 中并触发事件。

      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();
              
          
      
      
      

注意: 我们需要确保 EventsSubscriber.dll 和 EventsPublisher.dll 与 CrossDomainEvents.exe 位于同一文件夹中。可以在发布者和订阅者项目中使用 XCOPY 命令将 DLL 粘贴到 CrossDomainEvents 项目输出目录中。

【讨论】:

我写了一篇关于跨 AppDomain 的事件的文章。有时间就试试。 blog.vcillusion.co.in/…希望对您有所帮助!【参考方案2】:

您的代码示例失败的原因是事件声明和订阅它的代码在同一个类中。

在这种情况下,编译器通过让订阅事件的代码直接访问底层字段来“优化”代码。

基本上,而不是这样做(因为类之外的任何代码都必须这样做):

o.add_Event(delegateInstance);

这样做:

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

所以,我要问您的问题是:您的真实示例是否具有相同的代码布局?订阅事件的代码是否在声明事件的同一个类中?

如果是,那么下一个问题是:它必须在那里,还是真的应该被移出?通过将代码移出类,您可以使编译器使用add 和? remove 添加到您的对象的特殊方法。

如果您不能或不会移动代码,则另一种方式是负责为您的活动添加和删除代表:

private EventHandler _TimerExpired;
public event EventHandler TimerExpired

    add
    
        _TimerExpired += value;
    

    remove
    
        _TimerExpired -= value;
    

这会强制编译器在同一个类中的代码中调用 add 和 remove。

【讨论】:

【参考方案3】:

事件在远程处理中运行良好,但有一些复杂情况,我猜你遇到了其中之一。

主要问题是,为了让客户端订阅远程服务器对象的事件,框架需要在两端提供客户端和服务器的类型信息。如果没有这个,您可能会遇到一些与您看到的类似的远程处理异常。

有一些方法可以解决这个问题,包括手动使用观察者模式(相对于直接使用事件),或者提供在线路两端都可用的基类或接口。

我建议阅读此CodeProject article。在标题为“从远程对象引发事件”的部分中,它逐步介绍了如何使用远程事件,并对此问题进行了很好的描述。

基本上,主要的事情是确保您的处理程序遵循特定的准则,包括具体的、非虚拟的等。本文介绍了具体细节,并提供了工作示例。

【讨论】:

好的,但是如果带有事件的类型与带有处理程序的类型在同一个程序集中定义,并且如果两个 AppDomain 在同一台机器上的相同过程?它是一个 ASPNET 自定义主机。程序启动并调用 CreateApplicationHost()。 我也尝试使用与事件的发布者和订阅者相同的类型。该类型的一个实例是发布者,该类型在单独的 AppDomain 中的另一个实例是订阅者。结果相同。所以看起来“类型信息在电线的两端都不可用”不是我看到的问题。 如果它们是相同类型的对象,它应该可以工作。您是否订阅了公共的非虚拟方法(即:处理程序)?如果该方法是虚拟的,它通常会导致奇怪的问题。 是的,公共的,非虚拟的。我将发布重现问题的示例的完整来源。 OK,演示问题的例子上来了。

以上是关于如何跨 AppDomains 订阅事件(object.Event += handler;)的主要内容,如果未能解决你的问题,请参考以下文章

AppDomains之间如何最好地进行通信?

插件 AppDomains 解决方法

使用 appdomains 时无法设置同步上下文

跨 c#、c++ 和可能更多语言的进程间发布/订阅模型

跨 AppDomain 的自定义序列化

如何查找由ID启动的Azure事件