Как мога да се абонирам за събитие в AppDomains (object.Event += манипулатор;)

Имам проблема, описан в тази публикация в таблото за съобщения.

Имам обект, който се хоства в собствен 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;

    }
}

person Cheeso    schedule 07.09.2009    source източник


Отговори (3)


Причината, поради която вашият пример с код е неуспешен, е, че декларацията на събитието и кодът, който се абонира за него, са в един и същи клас.

В този случай компилаторът "оптимизира" кода, като кара кода, който се абонира за събитието, да има директен достъп до основното поле.

По принцип, вместо да правите това (както всеки код извън класа ще трябва):

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

Това принуждава компилатора да извиква добавянето и премахването дори от код в същия клас.

person Lasse V. Karlsen    schedule 10.10.2009

Събитията работят добре при дистанционно управление, но има някои усложнения и предполагам, че се натъквате на едно от тях.

Основният проблем е, че за да се абонира клиент за събитие на обект на отдалечен сървър, рамката трябва да има информация за типа както за клиента, така и за сървъра, налична и от двата края. Без това можете да получите някои отдалечени изключения, подобни на това, което виждате.

Има начини за заобикаляне на това, включително използване на модела на наблюдател ръчно (срещу директно използване на събитие) или предоставяне на базов клас или интерфейс, който е наличен от двете страни на жицата.

Препоръчвам да прочетете тази статия за CodeProject. Той преминава през използването на събития с отдалечено управление и има добро описание на този проблем в раздела, озаглавен „Извикване на събития от отдалечени обекти“.

По принцип основното нещо е да се уверите, че вашите манипулатори следват конкретни указания, включително да бъдат конкретни, невиртуални и т.н. Статията разглежда спецификата и предоставя работещи примери.

person Reed Copsey    schedule 07.09.2009
comment
добре, но въпросът с информацията за типа и в двата края се прилага, ако типът със събитието е дефиниран в същия сборник като типа с манипулатора и ако два AppDomains се изпълняват в рамките на един и същ процес на една и съща машина? Това е персонализиран хост на ASPNET. Програмата се стартира и извиква CreateApplicationHost(). - person Cheeso; 07.09.2009
comment
Също така го опитах, използвайки същия тип като издател и абонат на събитието. Един екземпляр от типа е издателят, друг екземпляр от типа в отделен AppDomain е абонатът. Същите резултати. Така че изглежда, че информацията за типа не е налична и в двата края на проводника, не е проблемът, който виждам. - person Cheeso; 07.09.2009
comment
Трябва да работи, ако са от един и същи тип обект. Абонирате ли се за публичен, не виртуален метод (т.е. манипулатор)? Ако методът е вирулен, той често причинява странни проблеми. - person Reed Copsey; 07.09.2009
comment
да, публично, не виртуално. Ще публикувам пълен източник на пример, който възпроизвежда проблема. - person Cheeso; 08.09.2009
comment
Добре, примерът, който демонстрира проблема, е готов. - person Cheeso; 08.09.2009

Домейнът на приложението е изолирана среда, в която се изпълняват приложения. С други думи, това е дял в процес на операционна система, където се намират едно или повече приложения.

  1. AppDomain ни позволява да зареждаме DLL по време на изпълнение.
  2. За комуникация между границата на „AppDomain“ типовете трябва да могат да се сериализират.
  3. Извлечете от клас MarshalByRefObject, който позволява достъп до обекти през границите на домейна на приложението в приложения, които поддържат отдалечено управление.
  4. Пълното име на DLL сборката се използва за зареждането му в AppDomain. Засега го поставяме в същата папка като основната програма.

В този раздел описахме подробно как да постигнем изпращане и получаване на събития през границата на домейна на приложението. Тук използваме споделената универсална библиотека с вече познати ни интерфейси и две отделни DLL файлове за издатели и абонати, заредени при събития по време на изпълнение и пожар в различни домейни.

За разбиране използваме четири отделни проекта.

  1. EventsCommon (Проект на библиотека от класове) Дефинира стандартни интерфейси за класове издател и абонат, а главният клас го използва за създаване на интерфейсни обекти.

        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 и внедрява свързан с издателя интерфейс IEventCommonGenerator от EventCommon.

      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 и внедрява свързан с абоната интерфейс IEventCommonCatcher от EventCommon.

      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 App Domain и EventsSubscriber в Subscriber App Domain, абонира събития от Publisher AppDomain в Subscriber App Domain и задейства събитието.

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

Забележка: Трябва да се уверим, че EventsSubscriber.dll и EventsPublisher.dll присъстват в същата папка като CrossDomainEvents.exe. Може да се направи с помощта на командата XCOPY в проекти на издател и абонат, за да поставите DLL в изходната директория на проекта CrossDomainEvents.

person vCillusion    schedule 29.05.2018
comment
Написах една статия за събития в AppDomain. Пробвай ако имаш време. blog.vcillusion.co.in/ Надежда помага! - person vCillusion; 30.05.2018