Pregunta

¿Cómo se suscribiría dinámicamente a un evento de C# para que, dada una instancia de objeto y un nombre de cadena que contenga el nombre del evento, se suscriba a ese evento y haga algo (escribir en la consola, por ejemplo) cuando se haya activado ese evento?

Parecería que usar Reflection esto no es posible y me gustaría evitar tener que usar Reflection.Emit si es posible, ya que actualmente (para mí) parece ser la única forma de hacerlo.

/EDITAR: No sé la firma del delegado necesaria para el evento, este es el meollo del problema.

/EDITAR 2: Aunque la contravarianza delegada parece un buen plan, no puedo hacer las suposiciones necesarias para utilizar esta solución.

¿Fue útil?

Solución

Puede compilar árboles de expresión para utilizar métodos void sin ningún argumento como controladores de eventos para eventos de cualquier tipo.Para dar cabida a otros tipos de controladores de eventos, debe asignar los parámetros del controlador de eventos a los eventos de alguna manera.

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

Otros consejos

No es una solución completamente general, pero si todos sus eventos son de la forma nula Foo (objeto o, t args), donde T deriva de EventArgs, entonces puede usar la infracción delegada para salirse con la suya.Así (donde la firma de KeyDown no es la misma que la de 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!");
    }

Es posible suscribirse a un evento usando Reflection

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

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

Ahora aquí estará el problema que tendrás que resolver.Los delegados necesarios para cada controlador de eventos tendrán firmas diferentes.Tendrá que encontrar la manera de crear estos métodos dinámicamente, lo que probablemente signifique Reflection.Emit, o tendrá que limitarse a un determinado delegado para poder manejarlo con código compilado.

Espero que esto ayude.

Pruebe LinFu: tiene un controlador de eventos universal que le permite vincularse a cualquier evento en tiempo de ejecución.Por ejemplo, aquí puede vincular un controlador al evento Click de un botón 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 le permite vincular a sus controladores a cualquier evento, independientemente de la firma del delegado.¡Disfrutar!

Lo puedes encontrar aquí: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
}

Recientemente escribí una serie de publicaciones de blog que describen eventos de prueba unitaria y una de las técnicas que analizo describe la suscripción dinámica a eventos.Utilicé reflexión y MSIL (emisión de código) para los aspectos dinámicos, pero todo esto está muy bien resumido.Usando la clase DynamicEvent, los eventos se pueden suscribir dinámicamente así:

EventPublisher publisher = new EventPublisher();

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

Una de las características del patrón que implementé fue que inyecta el nombre del evento en la llamada al controlador de eventos para que sepa qué evento se generó.Muy útil para pruebas unitarias.

El artículo del blog es bastante extenso ya que describe una técnica de prueba de unidad de eventos, pero se proporcionan el código fuente completo y las pruebas, y en la última publicación se detalla una descripción detallada de cómo se implementó la suscripción dinámica a eventos.

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

Lo que desea se puede lograr mediante la inyección de dependencia.Por ejemplo Bloque de aplicación de interfaz de usuario compuesta de Microsoft hace exactamente lo que describiste

Este método agrega a un evento, un controlador dinámico que llama a un método. OnRaised, pasando los parámetros del evento como una 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 tiene esta firma:

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

¿Quieres decir 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 );

Esto no realiza la suscripción, pero le dará al método para llamar.

Editar:

La publicación se aclaró después de esta respuesta, mi ejemplo no ayudará si no conoce el tipo.

Sin embargo, todos los eventos en .Net deben seguir el patrón de eventos predeterminado, por lo que, siempre que lo haya seguido, funcionará con el EventHandler básico.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top