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

Che*_*eso 13 .net events remoting

我遇到了此留言板帖子中描述的问题.

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

public class MyObject : MarshalByRefObject
{
    public event EventHandler TheEvent;
    ...
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

但是,当我这样做时:

// instance is an instance of an object that runs in a separate AppDomain
instance.TheEvent += this.Handler ; 
Run Code Online (Sandbox Code Playgroud)

...它编译得很好,但在运行时失败:

System.Runtime.Remoting.RemotingException: 
     Remoting cannot find field 'TheEvent' on type 'MyObject'.
Run Code Online (Sandbox Code Playgroud)

为什么?

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

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

    }
}
Run Code Online (Sandbox Code Playgroud)

ang*_*son 13

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

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

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

o.add_Event(delegateInstance);
Run Code Online (Sandbox Code Playgroud)

它这样做:

o.EventField = (DelegateType)Delegate.Combine(o.EventField, delegateInstance);
Run Code Online (Sandbox Code Playgroud)

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

如果是,那么接下来的问题是:它是否必须存在,或者它是否应该被移出?通过将代码移出类,您可以使编译器使用add和?remove添加到对象的特殊方法.

另一方面,如果您不能或不会移动代码,将接管添加和删除代表到您的活动的责任:

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

    remove
    {
        _TimerExpired -= value;
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Ree*_*sey 5

事件在远程处理中工作正常,但有一些复杂情况,我猜你正在遇到其中一个.

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

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

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

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