2009-09-07 12 views
12

Ich habe das Problem beschrieben in this message board post.Wie kann ich ein Ereignis über AppDomains abonnieren (object.Event + = Handler;)

Ich habe ein Objekt, das in seiner eigenen AppDomain gehostet wird.

public class MyObject : MarshalByRefObject 
{ 
    public event EventHandler TheEvent; 
    ... 
    ... 
} 

Ich möchte zu diesem Ereignis einen Handler hinzufügen. Der Handler wird in einer anderen AppDomain ausgeführt. Mein Verständnis ist, dass alles gut ist, Ereignisse werden über diese Grenze magisch mit .NET Remoting geliefert.

Aber, wenn ich dies tun:

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

... es kompiliert gut, aber zur Laufzeit nicht mit:

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

Warum?

EDIT: Quellcode der Arbeits App, die das Problem veranschaulicht:

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

    } 
} 

Antwort

13

Der Grund dafür, dass Ihr Codebeispiel fehlschlägt, besteht darin, dass die Ereignisdeklaration und der Code, der sie abonniert, derselben Klasse angehören.

In diesem Fall "optimiert" der Compiler den Code, indem der Code, der das Ereignis subskribiert, direkt auf das zugrunde liegende Feld zugreift.

Grundsätzlich statt, dies zu tun (wie jeder Code außerhalb der Klasse müssen):

o.add_Event(delegateInstance); 

es tut dies:

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

so die Frage, die ich für Sie ist this: Hat Ihr reales Beispiel das gleiche Code-Layout? Ist der Code, der das Ereignis abonniert, in derselben Klasse enthalten, die das Ereignis deklariert?

Wenn ja, dann ist die nächste Frage: Muss es da sein, oder sollte es wirklich aus ihm heraus bewegt werden?Wenn Sie den Code aus der Klasse entfernen, verwenden Sie den Compiler add und? remove spezielle Methoden, die zu Ihrem Objekt hinzugefügt werden.

Der andere Weg, wenn Sie nicht nicht den Code bewegen, oder wäre die Verantwortung für das Hinzufügen und Entfernen von Delegierten zu Ihrer Veranstaltung zu nehmen: das Add zu rufen

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

    remove 
    { 
     _TimerExpired -= value; 
    } 
} 

Dies zwingt den Compiler und entfernen Sie sogar aus Code innerhalb derselben Klasse.

+0

Ausgezeichnet! ~ Danke für die Erklärung. – Cheeso

5

Events funktionieren in Remoting, aber es gibt einige Komplikationen, und ich vermute, Sie laufen in einer von ihnen .

Das Hauptproblem besteht darin, dass das Framework für einen Client, der das Ereignis eines remoten Serverobjekts abonnieren muss, Typinformationen für den Client und den Server, die an beiden Enden verfügbar sind, benötigt. Ohne dies können Sie einige Remoting-Ausnahmen erhalten, die denen ähneln, die Sie sehen.

Es gibt verschiedene Möglichkeiten, das Observer-Muster manuell zu verwenden (im Gegensatz zur direkten Verwendung eines Ereignisses) oder eine Basisklasse oder Schnittstelle bereitzustellen, die auf beiden Seiten der Verbindung verfügbar ist.

Ich empfehle das Lesen CodeProject article. Es durchläuft Ereignisse mit Remoting und hat eine gute Beschreibung dieses Problems im Abschnitt "Ereignisse von Remote-Objekten auslösen".

Grundsätzlich ist es wichtig, sicherzustellen, dass Ihre Sachbearbeiter bestimmten Richtlinien folgen, einschließlich konkret, nicht virtuell usw. Der Artikel geht durch spezifische und bietet Arbeitsbeispiele.

+0

ok, aber gilt das Problem "Typinformationen auf beiden Seiten", wenn der Typ mit dem Ereignis in derselben Assembly wie der Typ mit dem Handler definiert ist und wenn zwei AppDomains innerhalb desselben Prozesses auf demselben ausgeführt werden Maschine? Es ist ein benutzerdefinierter ASPNET-Host. Das Programm startet und ruft CreateApplicationHost() auf. – Cheeso

+0

Ich versuchte es auch mit dem gleichen Typ wie der Herausgeber und Abonnent der Veranstaltung. Eine Instanz des Typs ist der Herausgeber, eine andere Instanz des Typs in einer separaten Anwendungsdomäne ist der Abonnent. Gleiche Ergebnisse. Es scheint also so, als ob die "Typinfo nicht an beiden Enden der Leitung verfügbar ist", ist nicht das Problem, das ich sehe. – Cheeso

+0

Es sollte funktionieren, wenn sie der gleiche Objekttyp sind. Abonnieren Sie eine öffentliche, nicht virtuelle Methode (dh den Handler)? Wenn die Methode virtual ist, verursacht es oft seltsame Probleme. –