Как я могу подписаться на событие в AppDomains (object.Event += handler;)

У меня возникла проблема, описанная в этом сообщении на доске объявлений.

У меня есть объект, размещенный в собственном 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'.

Почему?

EDIT: исходный код работающего приложения, демонстрирующий проблему:

// 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
хорошо, но применима ли информация о типе на обоих концах, если тип с событием определен в той же сборке, что и тип с обработчиком, и если два домена приложений работают в одном процессе на одном компьютере? Это настраиваемый узел 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 типы должны быть Serializable.
  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 в домен приложения издателя и EventsSubscriber в домен приложения подписчика, подписывает события домена приложения издателя в домен приложения подписчика и запускает событие.

      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