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

时间:2009-09-07 18:46:45

标签: .net events remoting

我遇到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(),\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;

    }
}

3 个答案:

答案 0 :(得分:13)

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

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

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

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

这会强制编译器调用添加和删除同一类中的代码。

答案 1 :(得分:5)

事件在远程处理方面运行良好,但有一些并发症,我猜你正在遇到其中一个。

主要问题是,对于客户端订阅远程服务器对象的事件,框架需要具有两端可用的客户端和服务器的类型信息。如果没有这个,你可以获得类似于你所看到的一些远程异常。

有很多方法可以解决这个问题,包括手动使用观察者模式(直接使用事件),或提供线路两侧可用的基类或接口。

我建议您阅读此CodeProject article。它通过使用远程处理事件,并在标题为“从远程对象中提升事件”一节中详细描述了此问题。

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

答案 2 :(得分:0)

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

  1. AppDomain允许我们在运行时加载DLL。
  2. 对于“AppDomain”边界之间的通信,类型应为Serializable。
  3. 派生自MarshalByRefObject类,该类允许在支持远程处理的应用程序中跨应用程序域边界访问对象。
  4. DLL程序集的全名用于将其加载到AppDomain中。现在,我们将它放在与主程序相同的文件夹中。
  5. 在本节中,我们详细介绍了如何通过Application Domain边界实现发送和接收事件。在这里,我们使用具有我们已知的接口的共享通用库,以及在运行时加载的两个单独的发布者和订阅者DLL以及跨域的激活事件。

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

    1. EventsCommon(类库项目) 它定义了Publisher和Subscriber类的标准接口,Main Class使用它来创建接口对象。

          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(类库项目) 它引用了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;
            }
        }
        
      2. EventsSubscriber(类库项目) 它引用EventCommon项目并从EventCommon实现与Subscriber相关的接口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);
                }
            }
        }
        
      3. CrossDomainEvents(主控制台应用程序) 它将EventsPublisher加载到Publisher AppDomain和EventsSubscriber到Subscriber AppDomain,将Publisher AppDomain的事件订阅到Subscriber 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();
                }
            }
        
        }
        
    2. 注意: 我们需要确保EventsSubscriber.dll和EventsPublisher.dll与CrossDomainEvents.exe位于同一文件夹中。可以使用Publisher和Subscriber项目中的XCOPY命令将DLL粘贴到CrossDomainEvents项目输出目录中。