Question

Comment vous abonneriez-vous dynamiquement à un événement C# afin qu'é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 sur la console par exemple) lorsque cet événement a été déclenché ?

Il semblerait qu'utiliser Reflection ne soit pas possible et j'aimerais éviter d'avoir à utiliser Reflection.Emit si possible, car cela me semble actuellement (pour moi) être la seule façon de le faire.

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

/MODIFIER 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

Était-ce utile?

La solution

Vous pouvez compiler des arborescences d'expressions pour utiliser des méthodes void sans aucun argument en tant que gestionnaires d'événements pour des événements de tout type.Pour prendre en charge d'autres types de gestionnaires d'événements, vous devez d'une manière ou d'une autre mapper les paramètres du gestionnaire d'événements aux événements.

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

Autres conseils

Ce n'est pas une solution complètement générale, mais si tous vos événements sont de la forme void foo (objet o, t args), où T dérive de EventArgs, alors vous pouvez utiliser la contravariation déléguée 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!");
    }

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énements auront des signatures différentes.Vous allez devoir trouver le moyen de créer ces méthodes de manière dynamique, ce qui signifie probablement Reflection.Emit, ou vous devrez vous limiter à un certain délégué afin de pouvoir le gérer avec du code compilé.

J'espère que cela t'aides.

Essayez LinFu : il dispose d'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 que vous pouvez lier un gestionnaire à l'événement Click 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é.Apprécier!

Vous pouvez le trouver ici: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
}

J'ai récemment écrit une série d'articles de blog décrivant les événements de tests unitaires, et l'une des techniques dont je discute décrit l'abonnement dynamique aux événements.J'ai utilisé la réflexion et MSIL (émission de code) pour les aspects dynamiques, mais tout cela est bien résumé.À l'aide de la classe DynamicEvent, les événements peuvent être abonnés dynamiquement comme suit :

EventPublisher publisher = new EventPublisher();

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

L'une des fonctionnalités du modèle que j'ai implémenté était qu'il injecte le nom de l'événement dans l'appel au gestionnaire d'événements afin que vous sachiez quel événement a été déclenché.Très utile pour les tests unitaires.

L'article du blog est assez long car il décrit une technique de test unitaire d'événements, mais le code source complet et les tests sont fournis, et une description détaillée de la façon dont l'abonnement dynamique aux événements a été implémenté est détaillée dans le dernier article.

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

Ce que vous voulez peut être réalisé en utilisant l'injection de dépendances.Par exemple Bloc d’application de l’interface utilisateur composite Microsoft fait exactement ce que vous avez décrit

Cette méthode ajoute à un événement, un gestionnaire dynamique qui appelle une méthode OnRaised, en passant les paramètres de l'événement sous forme de tableau d'objets :

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 porte cette signature :

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

Voulez-vous dire quelque chose comme :

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

Cela ne fait pas l'abonnement, mais donnera la méthode à appeler.

Modifier:

Le message a été clarifié après cette réponse, mon exemple ne vous aidera pas si vous ne connaissez pas le type.

Cependant, tous les événements dans .Net doivent suivre le modèle d'événement par défaut, donc tant que vous l'avez suivi, cela fonctionnera avec le EventHandler de base.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top