32 votes

Abonnement dynamique aux événements en C#

Comment s'abonner dynamiquement à un événement C# de sorte que, étant donné une instance d'objet et un nom de chaîne contenant le nom de l'événement, vous vous abonnez à cet événement et faites quelque chose (écrire dans la console par exemple) lorsque cet événement a été déclenché ?

Il semblerait que cela ne soit pas possible avec Reflection et j'aimerais éviter d'avoir à utiliser Reflection.Emit si possible, car cela me semble être la seule façon de procéder actuellement.

/EDIT : Je ne connais pas la signature du délégué nécessaire pour l'événement, c'est le cœur du problème.

/EDIT 2 : Bien que la contravariance des délégués semble être un bon plan, je ne peux pas faire l'hypothèse nécessaire pour utiliser cette solution.

29voto

Mark Cidade Points 53945

Vous pouvez compiler des arbres d'expression pour utiliser des méthodes void sans arguments comme gestionnaires d'événements de tout type. Pour prendre en compte d'autres types de gestionnaires d'événements, vous devez faire correspondre les paramètres du gestionnaire d'événements aux événements d'une manière ou d'une autre.

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

0 votes

Bon sang, les arbres d'expression sont si cool. J'ai écrit un code similaire une fois via Reflection.Emit. Quelle galère.

0 votes

Beau morceau de code. Pouvez-vous peut-être montrer comment le modifier pour qu'il supporte les arguments ? Je l'ai modifié pour obtenir la méthode avec les arguments, mais j'obtiens "variable 'x' of type 'System.String' referenced from scope '', but it is not defined" lorsque j'essaie de créer le délégué. Merci

0 votes

J'ai ajouté un autre exemple.

9voto

Matt Bishop Points 1187

Ce n'est pas une solution complètement générale, mais si tous vos événements sont de la forme void Foo(object o, T args) , où T dérive de EventArgs, alors vous pouvez utiliser la contravariance des délégués pour vous en sortir. Comme ceci (où la signature de KeyDown n'est pas la même que celle 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!");
    }

3voto

Nick Berardi Points 31361

Il est possible de s'abonner à un événement en utilisant Reflection

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

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

Voici maintenant le problème que vous allez devoir résoudre. Les délégués requis pour chaque gestionnaire d'événement auront des signatures différentes. Vous allez devoir trouver un moyen de créer ces méthodes dynamiquement, ce qui signifie probablement Reflection.Emit, ou vous allez devoir vous limiter à un certain délégué afin de pouvoir le gérer avec du code compilé.

J'espère que cela vous aidera.

2voto

plaureano Points 1105

Essayez LinFu - il possède un gestionnaire d'événements universel qui vous permet de vous lier à n'importe quel événement au moment de l'exécution. Par exemple, voici comment vous pouvez lier un gestionnaire à l'événement "clic" d'un bouton dynamique :

// 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 vous permet de lier vos gestionnaires à n'importe quel événement, quelle que soit la signature du délégué. Profitez-en !

Vous pouvez le trouver ici : http://www.codeproject.com/KB/cs/LinFuPart3.aspx

2voto

Erick Sgarbi Points 799
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
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X