2009-09-07 4 views
12

Je rencontre le problème décrit dans this message board post.Comment puis-je m'abonner à un événement sur AppDomains (object.Event + = handler;)

J'ai un objet hébergé dans son propre AppDomain.

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

Je voudrais ajouter un gestionnaire à cet événement. Le gestionnaire s'exécutera dans un AppDomain différent. Ma compréhension est que c'est tout bon, les événements sont livrés magiquement à travers cette frontière, avec .NET Remoting.

Mais, quand je fais ceci:

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

... il compile très bien mais échoue à l'exécution avec:

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

Pourquoi?

EDIT: le code source de l'application de travail qui illustre le problème:

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

    } 
} 

Répondre

13

La raison pour laquelle votre exemple de code échoue est que la déclaration d'événement et le code qui s'y abonne sont dans la même classe.

Dans ce cas, le compilateur "optimise" le code en faisant en sorte que le code qui souscrit à l'événement accède directement au champ sous-jacent.

En fait, au lieu de le faire (comme code externe de la classe devra):

o.add_Event(delegateInstance); 

il le fait:

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

, la question que je vous porte est ceci: Votre vrai exemple a-t-il la même disposition de code? Le code qui s'abonne à l'événement dans la même classe qui déclare l'événement? Si oui, alors la question suivante est: Est-ce que ça doit être là, ou devrait-il vraiment être retiré de celui-ci?En déplaçant le code hors de la classe, vous faites que le compilateur utilise le add et? remove méthodes spéciales qui sont ajoutées à votre objet.

L'autre façon, si vous ne pouvez pas ou ne bouges pas le code, serait de prendre en charge la responsabilité de l'ajout et la suppression des délégués à votre événement:

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

    remove 
    { 
     _TimerExpired -= value; 
    } 
} 

Cela force le compilateur d'appeler le module complémentaire et retirer même du code à l'intérieur de la même classe.

+0

Excellent! ~ Merci pour l'explication. – Cheeso

5

événements fonctionnent très bien dans Remoting, mais il y a des complications, et je devine que vous êtes en cours d'exécution dans l'un d'entre eux . Le problème principal est que, pour qu'un client s'abonne à un événement d'un objet serveur distant, le framework doit avoir des informations de type pour le client et le serveur disponibles aux deux extrémités. Sans cela, vous pouvez obtenir des exceptions à distance similaires à ce que vous voyez. Il existe des moyens de contourner ce problème, y compris l'utilisation manuelle du modèle d'observateur (par opposition à l'utilisation directe d'un événement) ou la fourniture d'une classe de base ou d'une interface disponible sur les deux côtés du réseau.

Je recommande de lire ceci CodeProject article. Il passe en revue les événements avec accès à distance et décrit correctement ce problème dans la section intitulée «Élever des événements à partir d'objets distants». Fondamentalement, l'essentiel est de s'assurer que vos gestionnaires suivent des directives spécifiques, y compris être concret, non-virtuel, etc. L'article se promène à travers des détails, et fournit des exemples de travail.

+0

ok, mais le problème "type de données sur les deux extrémités" s'applique si le type avec l'événement est défini dans le même assembly que le type avec le gestionnaire, et si deux AppDomains s'exécutent dans le même processus sur le même machine? C'est un hôte personnalisé ASPNET. Le programme démarre et appelle CreateApplicationHost(). – Cheeso

+0

J'ai également essayé en utilisant le même type à la fois l'éditeur et l'abonné de l'événement. Une instance du type est l'éditeur, une autre instance du type dans un AppDomain distinct est l'abonné. Mêmes résultats Donc, il semble que le "type d'information n'est pas disponible sur les deux extrémités du fil" n'est pas le problème que je vois. – Cheeso

+0

Cela devrait fonctionner s'ils sont du même type d'objet. Abonnez-vous à une méthode publique, non virtuelle (ie: le gestionnaire)? Si la méthode est virutale, elle provoque souvent des problèmes étranges. –

Questions connexes