Pergunta

Como você assinaria dinamicamente um evento C# para que, dada uma instância de Object e um nome String contendo o nome do evento, você se inscrevesse nesse evento e fizesse algo (escrever no console, por exemplo) quando esse evento fosse disparado?

Parece que usar o Reflection isso não é possível e eu gostaria de evitar ter que usar o Reflection.Emit se possível, já que atualmente (para mim) parece ser a única maneira de fazer isso.

/EDITAR: Não sei a assinatura do delegado necessária para o evento, esse é o cerne do problema

/EDITAR 2: Embora a contravariância delegada pareça um bom plano, não posso fazer a suposição necessária para usar esta solução

Foi útil?

Solução

Você pode compilar árvores de expressão para usar métodos void sem argumentos como manipuladores de eventos para eventos de qualquer tipo.Para acomodar outros tipos de manipuladores de eventos, você precisa mapear os parâmetros do manipulador de eventos para os eventos de alguma forma.

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

Outras dicas

Não é uma solução completamente geral, mas se todos os seus eventos forem do formulário vazio (objeto o, t args), onde t deriva de eventos, você pode usar a contravariância delegada para se safar.Assim (onde a assinatura do KeyDown não é a mesma do 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!");
    }

É possível assinar um evento usando Reflection

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

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

Agora, aqui está o problema que você terá que resolver.Os delegados necessários para cada manipulador de eventos terão assinaturas diferentes.Você terá que encontrar uma maneira de criar esses métodos dinamicamente, o que provavelmente significa Reflection.Emit, ou terá que se limitar a um determinado delegado para poder lidar com isso com o código compilado.

Espero que isto ajude.

Experimente o LinFu - ele possui um manipulador de eventos universal que permite vincular-se a qualquer evento em tempo de execução.Por exemplo, aqui você pode vincular um manipulador ao evento Click de um botão dinâmico:

// 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 permite vincular seus manipuladores a qualquer evento, independentemente da assinatura do delegado.Aproveitar!

Você pode encontrá-lo aqui:http://www.codeproject.com/KB/cs/LinFuPart3.aspx

public TestForm()
{
    Button b = new Button();

    this.Controls.Add(b);

    MethodInfo method = typeof(TestForm).GetMethod("Clickbutton",
    BindingFlags.NonPublic | BindingFlags.Instance);
    Type type = typeof(EventHandler);

    Delegate handler = Delegate.CreateDelegate(type, this, method);

    EventInfo eventInfo = cbo.GetType().GetEvent("Click");

    eventInfo.AddEventHandler(b, handler);

}

void Clickbutton(object sender, System.EventArgs e)
{
    // Code here
}

Recentemente escrevi uma série de postagens descrevendo eventos de teste de unidade, e uma das técnicas que discuto descreve a assinatura de eventos dinâmicos.Usei reflexão e MSIL (emissão de código) para os aspectos dinâmicos, mas tudo isso está bem resolvido.Usando a classe DynamicEvent, os eventos podem ser inscritos dinamicamente da seguinte forma:

EventPublisher publisher = new EventPublisher();

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

Um dos recursos do padrão que implementei foi que ele injeta o nome do evento na chamada do manipulador de eventos para que você saiba qual evento foi gerado.Muito útil para testes unitários.

O artigo do blog é bastante extenso, pois descreve uma técnica de teste de unidade de evento, mas o código-fonte completo e os testes são fornecidos, e uma descrição detalhada de como a assinatura de evento dinâmico foi implementada é detalhada na última postagem.

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

O que você deseja pode ser alcançado usando injeção de dependência.Por exemplo Bloco de aplicativo Microsoft Composite UI faz exatamente o que você descreveu

Este método adiciona a um evento, um manipulador dinâmico que chama um método OnRaised, passando os parâmetros do evento como uma matriz de objetos:

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 tem esta assinatura:

void OnRaised(string name, object[] parameters);

Você quer dizer algo como:

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

Isso não faz a assinatura, mas fornece o método a ser chamado.

Editar:

A postagem foi esclarecida após esta resposta, meu exemplo não ajudará se você não souber o tipo.

No entanto, todos os eventos em .Net devem seguir o padrão de evento padrão, portanto, desde que você o siga, isso funcionará com o EventHandler básico.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top