Dot-Net

如何跨 AppDomains 訂閱事件(object.Event += handler;)

  • May 29, 2018

我遇到了此留言板文章中描述的問題。

我有一個託管在自己的 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;

   }
}

您的程式碼範例失敗的原因是事件聲明和訂閱它的程式碼在同一個類中。

在這種情況下,編譯器通過使訂閱事件的程式碼直接訪問底層欄位來“優化”程式碼。

基本上,不要這樣做(因為類之外的任何程式碼都必須這樣做):

o.add_Event(delegateInstance);

它這樣做:

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

所以,我要問你的問題是:你的真實例子有相同的程式碼佈局嗎?訂閱事件的程式碼是否在聲明事件的同一類中?

如果是,那麼下一個問題是:它必須在那裡,還是真的應該被移出?通過將程式碼移出類,您可以讓編譯器使用addand ? remove添加到您的對象的特殊方法。

另一種方式,如果您不能或不會移動程式碼,則將負責為您的活動添加和刪除代表:

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

   remove
   {
       _TimerExpired -= value;
   }
}

這迫使編譯器甚至從同一類內的程式碼中呼叫添加和刪除。

引用自:https://stackoverflow.com/questions/1390564