Подписка на динамические события C#

Как бы вы динамически подписались на событие C#, чтобы, учитывая экземпляр Object и имя String, содержащее имя события, вы подписывались на это событие и что-то делали (например, писали в консоль), когда это событие было запущено?

Казалось бы, с помощью Reflection это невозможно, и я хотел бы избежать использования Reflection.Emit, если это возможно, поскольку в настоящее время (мне) это кажется единственным способом сделать это.

/EDIT: мне неизвестна подпись делегата, необходимая для события, в этом суть проблемы

/EDIT 2: Хотя контравариантность делегирования кажется хорошим планом, я не могу сделать предположение, необходимое для использования этого решения.


person DAC    schedule 05.09.2008    source источник


Ответы (8)


Вы можете скомпилировать деревья выражений для использования методов void без каких-либо аргументов в качестве обработчиков событий любого типа. Чтобы приспособить другие типы обработчиков событий, вы должны каким-то образом сопоставить параметры обработчика событий с событиями.

 using System;
 using System.Linq;
 using System.Linq.Expressions;
 using System.Reflection;

 class ExampleEventArgs : EventArgs
 {
    public int IntArg {get; set;}
 }

 class EventRaiser
 { 
     public event EventHandler SomethingHappened;
     public event EventHandler<ExampleEventArgs> SomethingHappenedWithArg;

     public void RaiseEvents()
     {
         if (SomethingHappened!=null) SomethingHappened(this, EventArgs.Empty);

         if (SomethingHappenedWithArg!=null) 
         {
            SomethingHappenedWithArg(this, new ExampleEventArgs{IntArg = 5});
         }
     }
 }

 class Handler
 { 
     public void HandleEvent() { Console.WriteLine("Handler.HandleEvent() called.");}
     public void HandleEventWithArg(int arg) { Console.WriteLine("Arg: {0}",arg);    }
 }

 static class EventProxy
 { 
     //void delegates with no parameters
     static public Delegate Create(EventInfo evt, Action d)
     { 
         var handlerType = evt.EventHandlerType;
         var eventParams = handlerType.GetMethod("Invoke").GetParameters();

         //lambda: (object x0, EventArgs x1) => d()
         var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x"));
         var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"));
         var lambda = Expression.Lambda(body,parameters.ToArray());
         return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
     }

     //void delegate with one parameter
     static public Delegate Create<T>(EventInfo evt, Action<T> d)
     {
         var handlerType = evt.EventHandlerType;
         var eventParams = handlerType.GetMethod("Invoke").GetParameters();

         //lambda: (object x0, ExampleEventArgs x1) => d(x1.IntArg)
         var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x")).ToArray();
         var arg    = getArgExpression(parameters[1], typeof(T));
         var body   = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"), arg);
         var lambda = Expression.Lambda(body,parameters);
         return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
     }

     //returns an expression that represents an argument to be passed to the delegate
     static Expression getArgExpression(ParameterExpression eventArgs, Type handlerArgType)
     {
        if (eventArgs.Type==typeof(ExampleEventArgs) && handlerArgType==typeof(int))
        {
           //"x1.IntArg"
           var memberInfo = eventArgs.Type.GetMember("IntArg")[0];
           return Expression.MakeMemberAccess(eventArgs,memberInfo);
        }

        throw new NotSupportedException(eventArgs+"->"+handlerArgType);
     }
 }


 static class Test
 {
     public static void Main()
     { 
        var raiser  = new EventRaiser();
        var handler = new Handler();

        //void delegate with no parameters
        string eventName = "SomethingHappened";
        var eventinfo = raiser.GetType().GetEvent(eventName);
        eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,handler.HandleEvent));

        //void delegate with one parameter
        string eventName2 = "SomethingHappenedWithArg";
        var eventInfo2 = raiser.GetType().GetEvent(eventName2);
        eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,handler.HandleEventWithArg));

        //or even just:
        eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,()=>Console.WriteLine("!")));  
        eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,i=>Console.WriteLine(i+"!")));

        raiser.RaiseEvents();
     }
 }
person Mark Cidade    schedule 05.09.2008
comment
Черт, деревья выражений такие классные. Однажды я написал аналогичный код через Reflection.Emit. Какая боль. - person Christian Klauser; 11.02.2009
comment
Отличный кусок кода. Можете ли вы показать, как изменить его для поддержки аргументов? Я изменил его, чтобы получить метод с аргументами, но я получаю переменную «x» типа «System.String», на которую ссылается область видимости, но она не определена, когда я пытаюсь создать делегат. Спасибо - person monkey_p; 23.01.2012
comment
Готово — я добавил еще один пример. - person Mark Cidade; 23.01.2012
comment
Итак, как это будет работать для обычного шаблона Eventhandler: (отправитель объекта, Eventargs e)? - person Roberto Bonini; 25.02.2012
comment
Вам нужно будет добавить перегрузку, Delegate Create<T1,T2>(EventInfo evt, Action<T1,T2> d). - person Mark Cidade; 05.03.2012
comment
Скажем, у меня есть следующее в моем Observable eventInfo2.AddEventHandler(raiser,EventProxy.Create‹int›(eventInfo2,i=›Console.WriteLine(i+!))); Как именно передать аргументы? Я предполагаю, что передаю его Creat()? Но что происходит с ним оттуда? Благодаря. - person Nestor Ledon; 24.03.2013
comment
Передайте SomethingHappenedWithArg() - person Mark Cidade; 25.03.2013

Это не совсем общее решение, но если все ваши события имеют форму void Foo(object o, T args) , где T является производным от EventArgs, то вы можете использовать контравариантность делегата, чтобы избежать этого. Вот так (где подпись KeyDown не совпадает с подписью Click):

    public Form1()
    {
        Button b = new Button();
        TextBox tb = new TextBox();

        this.Controls.Add(b);
        this.Controls.Add(tb);
        WireUp(b, "Click", "Clickbutton");
        WireUp(tb, "KeyDown", "Clickbutton");
    }

    void WireUp(object o, string eventname, string methodname)
    {
        EventInfo ei = o.GetType().GetEvent(eventname);

        MethodInfo mi = this.GetType().GetMethod(methodname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);

        Delegate del = Delegate.CreateDelegate(ei.EventHandlerType, this, mi);

        ei.AddEventHandler(o, del);

    }
    void Clickbutton(object sender, System.EventArgs e)
    {
        MessageBox.Show("hello!");
    }
person Matt Bishop    schedule 05.09.2008

Можно подписаться на событие с помощью Reflection

var o = new SomeObjectWithEvent;
o.GetType().GetEvent("SomeEvent").AddEventHandler(...);

http://msdn.microsoft.com/en-us/library/system.reflection.eventinfo.addeventhandler.aspx

Теперь вот будет проблема, которую вам придется решить. Делегаты, необходимые для каждого обработчика событий, будут иметь разные подписи. Вам придется найти способ динамического создания этих методов, что, вероятно, означает Reflection.Emit, или вам придется ограничить себя определенным делегатом, чтобы вы могли обрабатывать его с помощью скомпилированного кода.

Надеюсь это поможет.

person Nick Berardi    schedule 05.09.2008

Попробуйте LinFu — у него есть универсальный обработчик событий, который позволяет привязываться к любому событию во время выполнения. Например, вот вы можете привязать обработчик к событию Click динамической кнопки:

// Note: The CustomDelegate signature is defined as:
// public delegate object CustomDelegate(params object[] args);
CustomDelegate handler = delegate
                         {
                           Console.WriteLine("Button Clicked!");
                           return null;
                         };

Button myButton = new Button();
// Connect the handler to the event
EventBinder.BindToEvent("Click", myButton, handler);

LinFu позволяет вам привязывать свои обработчики к любому событию, независимо от подписи делегата. Наслаждаться!

Вы можете найти его здесь: http://www.codeproject.com/KB/cs/LinFuPart3.aspx

person plaureano    schedule 18.12.2008

Недавно я написал серию сообщений в блоге, описывающих события модульного тестирования, и один из методов, которые я обсуждаю, описывает динамическую подписку на события. Я использовал отражение и MSIL (генерацию кода) для динамических аспектов, но все это хорошо упаковано. Используя класс DynamicEvent, события могут быть динамически подписаны следующим образом:

EventPublisher publisher = new EventPublisher();

foreach (EventInfo eventInfo in publisher.GetType().GetEvents())
{
    DynamicEvent.Subscribe(eventInfo, publisher, (sender, e, eventName) =>
    {
        Console.WriteLine("Event raised: " + eventName);
    });
}

Одной из особенностей шаблона, который я реализовал, было то, что он вставляет имя события в вызов обработчика событий, чтобы вы знали, какое событие было вызвано. Очень полезно для модульного тестирования.

Статья в блоге довольно длинная, так как описывает технику модульного тестирования событий, но полный исходный код и тесты предоставляются, а подробное описание того, как была реализована динамическая подписка на события, подробно описано в последнем посте.

http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/

person Tim Lloyd    schedule 22.04.2010
comment
@Robert Спасибо за комментарий, вчера у нас произошел сбой после исправления нашего веб-сервера. - person Tim Lloyd; 22.09.2010

То, что вы хотите, может быть достигнуто с помощью внедрения зависимостей. Например, блок приложения Microsoft Composite UI делает именно то, что вы описали.

person aku    schedule 05.09.2008

Этот метод добавляет к событию динамический обработчик, который вызывает метод OnRaised, передавая параметры события в виде массива объектов:

void Subscribe(object source, EventInfo ev)
{
    var eventParams = ev.EventHandlerType.GetMethod("Invoke").GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray();
    var eventHandler = Expression.Lambda(ev.EventHandlerType,
        Expression.Call(
            instance: Expression.Constant(this),
            method: typeof(EventSubscriber).GetMethod(nameof(OnRaised), BindingFlags.NonPublic | BindingFlags.Instance),
            arg0: Expression.Constant(ev.Name),
            arg1: Expression.NewArrayInit(typeof(object), eventParams.Select(p => Expression.Convert(p, typeof(object))))),
        eventParams);
    ev.AddEventHandler(source, eventHandler.Compile());
}

OnRaised имеет эту подпись:

void OnRaised(string name, object[] parameters);
person Edward Brey    schedule 11.04.2018

Вы имеете в виду что-то вроде:

//reflect out the method to fire as a delegate
EventHandler eventDelegate = 
   ( EventHandler ) Delegate.CreateDelegate(
       typeof( EventHandler ),    //type of event delegate
       objectWithEventSubscriber, //instance of the object with the matching method
       eventSubscriberMethodName, //the name of the method
       true );

Это не делает подписку, но дает метод для вызова.

Редактировать:

Сообщение было уточнено после этого ответа, мой пример не поможет, если вы не знаете тип.

Однако все события в .Net должны следовать шаблону событий по умолчанию, поэтому, если вы следуете ему, это будет работать с базовым обработчиком событий.

person Keith    schedule 05.09.2008