51 votes

Événements ponctuels à l'aide de Lambda en C#

Je me surprends à faire ce genre de choses assez souvent :-)

 EventHandler eh = null;  //can't assign lambda directly since it uses eh
 eh = (s, args) =>
 {
     //small snippet of code here

     ((SomeType)s).SomeEvent -= eh;
 }
 variableOfSomeType.SomeEvent += eh;

En fait, je veux seulement attacher un gestionnaire d'événement pour écouter un coup de l'événement, je ne veux plus rester attaché après cela. Très souvent, ce "bout de code" n'est qu'une ligne.

Mon esprit est un peu engourdi, je suis sûr qu'il doit y avoir quelque chose que je peux faire pour ne pas avoir à répéter tout cela. Gardez à l'esprit que EventHandler pourrait bien être EventHandler<T> .

Avez-vous une idée de la façon dont je peux mettre de l'ordre dans la partie répétitive du code et laisser le bout de code dans un Lambda ?

0 votes

Ça sent mauvais, je suis d'accord. J'ai hâte de voir les réponses ;)

0 votes

Que diriez-vous d'attacher un gestionnaire d'événement permanent qui invoque des "gestionnaires d'événements ponctuels" ajoutés à une file d'attente ?

0 votes

Intéressant ; nous avons fait quelque chose de similaire dans PushLINQ ; mais pour la réutilisation, il y a des problèmes ; les lambdas ne peuvent pas détecter les événements, et sans l'événement, l'inférence de type est une douleur - et sans inférence de type, le gestionnaire ne peut pas être un lambda... amusant.

9voto

Jordão Points 29221

Vous pouvez utiliser la réflexion :

public static class Listener {

  public static void ListenOnce(this object eventSource, string eventName, EventHandler handler) {
    var eventInfo = eventSource.GetType().GetEvent(eventName);
    EventHandler internalHandler = null;
    internalHandler = (src, args) => {
      eventInfo.RemoveEventHandler(eventSource, internalHandler);
      handler(src, args);
    };
    eventInfo.AddEventHandler(eventSource, internalHandler);
  }

  public static void ListenOnce<TEventArgs>(this object eventSource, string eventName, EventHandler<TEventArgs> handler) where TEventArgs : EventArgs {
    var eventInfo = eventSource.GetType().GetEvent(eventName);
    EventHandler<TEventArgs> internalHandler = null;
    internalHandler = (src, args) => {
      eventInfo.RemoveEventHandler(eventSource, internalHandler);
      handler(src, args);
    };
    eventInfo.AddEventHandler(eventSource, internalHandler);
  }

}

Utilisez-le comme ça :

variableOfSomeType.ListenOnce("SomeEvent", 
  (s, args) => Console.WriteLine("I should print only once!"));

variableOfSomeType.ListenOnce<InterestingEventArgs>("SomeOtherEvent", 
  (s, args) => Console.WriteLine("I should print only once!"));

1 votes

Très bien. La seule chose que je changerais est dans internalHandler la première chose que je ferais est d'appeler le RemoveEventHandler (et si le handler l'appel lance une exception)

0 votes

@PLopes : c'est très logique, je vais faire le changement.

8voto

dtb Points 104373

Vous pourriez attacher un gestionnaire d'événement permanent à l'événement. Le gestionnaire d'événement invoque ensuite des "gestionnaires d'événements ponctuels" qui sont ajoutés à une file d'attente interne :

OneShotHandlerQueue<EventArgs> queue = new OneShotHandlerQueue<EventArgs>();

Test test = new Test();

// attach permanent event handler
test.Done += queue.Handle;

// add a "one shot" event handler
queue.Add((sender, e) => Console.WriteLine(e));
test.Start();

// add another "one shot" event handler
queue.Add((sender, e) => Console.WriteLine(e));
test.Start();

Code :

class OneShotHandlerQueue<TEventArgs> where TEventArgs : EventArgs {
    private ConcurrentQueue<EventHandler<TEventArgs>> queue;
    public OneShotHandlerQueue() {
        this.queue = new ConcurrentQueue<EventHandler<TEventArgs>>();
    }
    public void Handle(object sender, TEventArgs e) {
        EventHandler<TEventArgs> handler;
        if (this.queue.TryDequeue(out handler) && (handler != null))
            handler(sender, e);
    }
    public void Add(EventHandler<TEventArgs> handler) {
        this.queue.Enqueue(handler);
    }
}

Classe de test :

class Test {
    public event EventHandler Done;
    public void Start() {
        this.OnDone(new EventArgs());
    }
    protected virtual void OnDone(EventArgs e) {
        EventHandler handler = this.Done;
        if (handler != null)
            handler(this, e);
    }
}

0 votes

Ajoute juste un autre niveau d'indirection. Brillant !

0 votes

Assez bon. J'ai été opérationnel en 120 secondes environ à partir du moment où j'ai trouvé cette réponse.

0 votes

Pour une file d'attente avec plus d'une entrée, while(queue.TryDeqeue(out handler)) handler?.(this, e)

7voto

Reed Copsey Points 315315

Si vous pouvez utiliser le Extensions réactives pour .NET vous pouvez simplifier cela.

Vous pouvez faire un Observable à partir d'un événement et n'écouter que le premier élément en utilisant .Take(1) pour réaliser votre petit bout de code. Tout le processus se résume ainsi à quelques lignes de code.


Edit : Afin de démontrer, j'ai fait un programme d'exemple complet (je vais le coller ci-dessous).

J'ai déplacé la création et l'abonnement de l'observable dans une méthode ( HandleOneShot ). Cela vous permet de faire ce que vous essayez de faire avec un seul appel de méthode. Pour la démonstration, j'ai créé une classe avec deux propriétés qui implémente INotifyPropertyChanged, et j'écoute l'appel à la méthode premièrement l'événement de changement de propriété, en écrivant dans la console quand il se produit.

Cela prend votre code, et le change en :

HandleOneShot<SomeEventArgs>(variableOfSomeType, "SomeEvent",  e => { 
                    // Small snippet of code here
                }); 

Notez que toutes les opérations d'abonnement et de désabonnement se déroulent automatiquement pour vous en coulisses. Il n'est pas nécessaire de gérer l'abonnement manuellement - il suffit de s'abonner à l'Observable, et Rx s'en occupe pour vous.

Lorsqu'il est exécuté, ce code s'imprime :

Setup...
Setting first property...
 **** Prop2 Changed! /new val
Setting second property...
Setting first property again.
Press ENTER to continue...

Vous n'avez qu'une seule chance de déclencher votre événement.

namespace ConsoleApplication1
{
    using System;
    using System.ComponentModel;
    using System.Linq;

    class Test : INotifyPropertyChanged
    {
        private string prop2;
        private string prop;
        public string Prop
        {
            get {
                return prop;
            }
            set
            {
                if (prop != value)
                {
                    prop = value;
                    if (PropertyChanged!=null)
                        PropertyChanged(this, new PropertyChangedEventArgs("Prop"));
                }
            }
        }

        public string Prop2
        {
            get
            {
                return prop2;
            }
            set
            {
                if (prop2 != value)
                {
                    prop2 = value;
                    if (PropertyChanged != null)
                        PropertyChanged(this, new PropertyChangedEventArgs("Prop2"));
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    class Program
    {
        static void HandleOneShot<TEventArgs>(object target, string eventName, Action<TEventArgs> action)  where TEventArgs : EventArgs
        {
            var obsEvent = Observable.FromEvent<TEventArgs>(target, eventName).Take(1);
            obsEvent.Subscribe(a => action(a.EventArgs));
        }

        static void Main(string[] args)
        {
            Test test = new Test();

            Console.WriteLine("Setup...");
            HandleOneShot<PropertyChangedEventArgs>(
                test, 
                "PropertyChanged", 
                e =>
                    {
                        Console.WriteLine(" **** {0} Changed! {1}/{2}!", e.PropertyName, test.Prop, test.Prop2);
                    });

            Console.WriteLine("Setting first property...");
            test.Prop2 = "new value";
            Console.WriteLine("Setting second property...");
            test.Prop = "second value";
            Console.WriteLine("Setting first property again...");
            test.Prop2 = "other value";

            Console.WriteLine("Press ENTER to continue...");
            Console.ReadLine();
        }
    }
}

0 votes

Je ne me suis pas encore vraiment plongé dans les extensions réactives, il est certain que cela fournirait la fonctionnalité que @dtb semblait décrire mais laisserait toujours un délégué sur l'événement qui se déclenche mais ne va nulle part ? Mon objectif est de supprimer l'attachement inutile ou dois-je mieux comprendre les trucs réactifs ?

0 votes

@AnthonyWJones : C'est long, mais c'est un code entièrement fonctionnel. Vous aurez besoin du framework Rx pour qu'il fonctionne, et ajouter des références aux assemblages Rx, cependant : msdn.microsoft.com/fr/us/devlabs/ee794896.aspx

2voto

Juliet Points 40758

Un autre utilisateur a rencontré un problème très similaire et je crois que la solution proposée dans ce fil s'applique ici.

En particulier, ce que vous avez est pas une instance du modèle de publication/abonnement, sa file d'attente de messages. Il est assez facile de créer votre propre file d'attente de messages à l'aide d'un fichier de type Queue{EventHandler} où vous retirez les événements de la liste au fur et à mesure que vous les invoquez.

Ainsi, au lieu de se connecter à un gestionnaire d'événements, vos événements "one-shot" doivent exposer une méthode permettant aux clients d'ajouter une fonction à la file d'attente des messages.

2 votes

Cela suppose cependant que vous écrivez la classe pour "variableOfSomeType", et qu'il ne s'agit pas d'un cadre ou d'un type tiers. Si c'est le cas, cela ne fonctionne pas.

1voto

Jonathan Allen Points 23540

Est-ce que ça marche ? Si oui, alors je dis allez-y. Pour un événement ponctuel, cela semble assez élégant.

Ce que j'aime...

  • Si s est récupéré, le gestionnaire d'événement le sera aussi.
  • Le code de détachement se trouve juste à côté du code d'attachement, ce qui permet de voir facilement ce que vous êtes en train de faire.

Vous pouvez peut-être le généraliser, mais je ne sais pas vraiment comment faire car je n'arrive pas à obtenir un pointeur sur un événement.

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