web-dev-qa-db-fra.com

C # événement rebounce

J'écoute un message d'événement matériel, mais je dois le supprimer pour éviter trop de requêtes.

Il s’agit d’un événement matériel qui envoie l’état de la machine et je dois le stocker dans une base de données à des fins statistiques. Il arrive parfois que son état change très souvent (scintillement?). Dans ce cas, je souhaite ne stocker qu'un statut "stable" et je veux le mettre en œuvre simplement en attendant 1-2 avant de stocker le statut dans la base de données.

Ceci est mon code:

private MachineClass connect()
{
    try
    {
        MachineClass rpc = new MachineClass();
        rpc.RxVARxH += eventRxVARxH;
        return rpc;
    }
    catch (Exception e1)
    {
        log.Error(e1.Message);
        return null;
    }
}

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
}

J'appelle ce comportement "debounce": attendez quelques fois pour vraiment faire son travail: si le même événement est déclenché à nouveau pendant le temps d'anti-rebond, je dois rejeter la première demande et commencer à attendre le temps d'anti-rebond pour terminer le deuxième événement.

Quel est le meilleur choix pour le gérer? Tout simplement une minuterie one-shot?

Pour expliquer la fonction "debounce", veuillez vous reporter à l'implémentation javascript des événements clés: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

26
Tobia

Ce n'est pas une demande triviale de coder à partir de zéro car il y a plusieurs nuances. Un scénario similaire consiste à surveiller un FileSystemWatcher et à attendre que la situation se soit calmée après une copie volumineuse avant d'essayer d'ouvrir les fichiers modifiés.

Les extensions réactives dans .NET 4.5 ont été créées pour gérer exactement ces scénarios. Vous pouvez facilement les utiliser pour fournir de telles fonctionnalités avec des méthodes telles que accélérateur , tampon , fenêtre ou exemple . Vous publiez les événements sur un Sujet , vous lui appliquez l'une des fonctions de fenêtrage, par exemple pour obtenir une notification uniquement s'il n'y a pas eu d'activité pendant X secondes ou Y événements, puis vous vous abonnez à la notification.

Subject<MyEventData> _mySubject=new Subject<MyEventData>();
....
var eventSequenc=mySubject.Throttle(TimeSpan.FromSeconds(1))
                          .Subscribe(events=>MySubscriptionMethod(events));

Throttle renvoie le dernier événement dans une fenêtre glissante, uniquement s’il n’y avait aucun autre événement dans la fenêtre. Tout événement réinitialise la fenêtre.

Vous pouvez trouver un très bon aperçu des fonctions décalées ici

Lorsque votre code reçoit l'événement, il vous suffit de l'envoyer au sujet avec OnNext:

_mySubject.OnNext(MyEventData);

Si votre événement matériel apparaît comme un événement .NET typique, vous pouvez ignorer la publication d'objet et manuelle avec Observable.FromEventPattern , comme indiqué ici :

var mySequence = Observable.FromEventPattern<MyEventData>(
    h => _myDevice.MyEvent += h,
    h => _myDevice.MyEvent -= h);  
_mySequence.Throttle(TimeSpan.FromSeconds(1))
           .Subscribe(events=>MySubscriptionMethod(events));

Vous pouvez également créer des observables à partir de tâches, combiner des séquences d’événements avec des opérateurs LINQ pour demander, par exemple, des paires d’événements matériels différents avec Zip, utiliser une autre source d’évènements pour relier la manette/tampon, etc.

Reactive Extensions est disponible sous forme de paquet NuGet _, il est donc très facile de les ajouter à votre projet.

Le livre de Stephen Cleary " Concurrency in C # Cookbook } _" est une très bonne ressource sur les extensions réactives, entre autres, et explique comment vous pouvez l'utiliser et comment il s'intègre au reste des APIs .NET comme tâches, événements, etc. 

Introduction to Rx est une excellente série d'articles (c'est de là que j'ai copié les exemples), avec plusieurs exemples.

METTRE &AGRAVE; JOUR

En utilisant votre exemple spécifique, vous pourriez faire quelque chose comme:

IObservable<MachineClass> _myObservable;

private MachineClass connect()
{

    MachineClass rpc = new MachineClass();
   _myObservable=Observable
                 .FromEventPattern<MachineClass>(
                            h=> rpc.RxVARxH += h,
                            h=> rpc.RxVARxH -= h)
                 .Throttle(TimeSpan.FromSeconds(1));
   _myObservable.Subscribe(machine=>eventRxVARxH(machine));
    return rpc;
}

Ceci peut être grandement amélioré bien sûr - à la fois l'observable et l'abonnement doivent être supprimés à un moment donné. Ce code suppose que vous ne contrôlez qu'un seul périphérique. Si vous avez plusieurs périphériques, vous pouvez créer l'observable à l'intérieur de la classe de sorte que chaque MachineClass expose et dispose de son propre observable.

30
Panagiotis Kanavos

J'ai utilisé cela pour dénoncer des événements avec un certain succès:

public static Action<T> Debounce<T>(this Action<T> func, int milliseconds = 300)
{
    var last = 0;
    return arg =>
    {
        var current = Interlocked.Increment(ref last);
        Task.Delay(milliseconds).ContinueWith(task =>
        {
            if (current == last) func(arg);
            task.Dispose();
        });
    };
}

Usage

Action<int> a = (arg) =>
{
    // This was successfully debounced...
    Console.WriteLine(arg);
};
var debouncedWrapper = a.Debounce<int>();

while (true)
{
    var rndVal = rnd.Next(400);
    Thread.Sleep(rndVal);
    debouncedWrapper(rndVal);
}

Ce n'est peut-être pas aussi robuste que ce qu'il y a dans RX mais il est facile à comprendre et à utiliser.

28
Mike Ward

Récemment, je faisais de la maintenance sur une application qui ciblait une ancienne version du framework .NET (v3.5). 

Je ne pouvais utiliser ni Reactive Extensions, ni Task Parallel Library, mais j'avais besoin d'une méthode agréable, propre et cohérente pour supprimer les événements. Voici ce que je suis venu avec:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace MyApplication
{
    public class Debouncer : IDisposable
    {
        readonly TimeSpan _ts;
        readonly Action _action;
        readonly HashSet<ManualResetEvent> _resets = new HashSet<ManualResetEvent>();
        readonly object _mutex = new object();

        public Debouncer(TimeSpan timespan, Action action)
        {
            _ts = timespan;
            _action = action;
        }

        public void Invoke()
        {
            var thisReset = new ManualResetEvent(false);

            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var otherReset = _resets.First();
                    _resets.Remove(otherReset);
                    otherReset.Set();
                }

                _resets.Add(thisReset);
            }

            ThreadPool.QueueUserWorkItem(_ =>
            {
                try
                {
                    if (!thisReset.WaitOne(_ts))
                    {
                        _action();
                    }
                }
                finally
                {
                    lock (_mutex)
                    {
                        using (thisReset)
                            _resets.Remove(thisReset);
                    }
                }
            });
        }

        public void Dispose()
        {
            lock (_mutex)
            {
                while (_resets.Count > 0)
                {
                    var reset = _resets.First();
                    _resets.Remove(reset);
                    reset.Set();
                }
            }
        }
    }
}

Voici un exemple d'utilisation dans un formulaire Windows comportant une zone de texte de recherche:

public partial class Example : Form 
{
    private readonly Debouncer _searchDebouncer;

    public Example()
    {
        InitializeComponent();
        _searchDebouncer = new Debouncer(TimeSpan.FromSeconds(.75), Search);
        txtSearchText.TextChanged += txtSearchText_TextChanged;
    }

    private void txtSearchText_TextChanged(object sender, EventArgs e)
    {
        _searchDebouncer.Invoke();
    }

    private void Search()
    {
        if (InvokeRequired)
        {
            Invoke((Action)Search);
            return;
        }

        if (!string.IsNullOrEmpty(txtSearchText.Text))
        {
            // Search here
        }
    }
}
7
Ronnie Overby

La réponse de Panagiotis est certes correcte, mais je voulais donner un exemple plus simple, car il m'a fallu un certain temps pour savoir comment le faire fonctionner. Mon scénario est le suivant: un utilisateur tape dans un champ de recherche. Comme il tape, nous souhaitons effectuer des appels api pour renvoyer des suggestions de recherche. Par conséquent, nous souhaitons annuler les appels api afin qu’ils ne passent pas un à chaque fois qu’ils tapent un caractère.

J'utilise Xamarin.Android, cependant cela devrait s'appliquer à n'importe quel scénario C # ...

private Subject<string> typingSubject = new Subject<string> ();
private IDisposable typingEventSequence;

private void Init () {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            searchText.TextChanged += SearchTextChanged;
            typingEventSequence = typingSubject.Throttle (TimeSpan.FromSeconds (1))
                .Subscribe (query => suggestionsAdapter.Get (query));
}

private void SearchTextChanged (object sender, TextChangedEventArgs e) {
            var searchText = layoutView.FindViewById<EditText> (Resource.Id.search_text);
            typingSubject.OnNext (searchText.Text.Trim ());
        }

public override void OnDestroy () {
            if (typingEventSequence != null)
                typingEventSequence.Dispose ();
            base.OnDestroy ();
        }

Lorsque vous initialisez l'écran/la classe pour la première fois, vous créez votre événement pour écouter l'utilisateur qui tape (SearchTextChanged), puis vous configurez également un abonnement de limitation lié au "typingSubject".

Ensuite, dans votre événement SearchTextChanged, vous pouvez appeler typingSubject.OnNext et transmettre le texte du champ de recherche. Après la période anti-rebond (1 seconde), il appellera l'événement abonné (suggestionsAdapter.Get dans notre cas.)

Enfin, lorsque l'écran est fermé, assurez-vous de disposer de l'abonnement!

5
Justin

J'ai rencontré des problèmes avec cela. J'ai essayé chacune des réponses ici, et depuis que je suis dans une application universelle Xamarin, il me semble qu'il me manque certaines choses qui sont requises dans chacune de ces réponses, et je ne voulais pas ajouter d'autres packages ou bibliothèques. Ma solution fonctionne exactement comme je l’attendais, et je n’ai rencontré aucun problème avec celle-ci. J'espère que ça aide quelqu'un.

    using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OrderScanner.Models
{
    class Debouncer
    {
        private List<CancellationTokenSource> StepperCancelTokens = new List<CancellationTokenSource>();
        private int MillisecondsToWait;

        public Debouncer(int millisecondsToWait = 300)
        {
            this.MillisecondsToWait = millisecondsToWait;
        }

        public void Debouce(Action func)
        {
            CancelAllStepperTokens(); // Cancel all api requests;
            var newTokenSrc = new CancellationTokenSource();
            StepperCancelTokens.Add(newTokenSrc);
            Task.Delay(MillisecondsToWait, newTokenSrc.Token).ContinueWith(task => // Create new request
            {
                if (!newTokenSrc.IsCancellationRequested) // if it hasn't been cancelled
                {
                    func(); // run
                    CancelAllStepperTokens(); // Cancel any that remain (there shouldn't be any)
                    StepperCancelTokens = new List<CancellationTokenSource>(); // set to new list
                }
            });
        }

        private void CancelAllStepperTokens()
        {
            foreach (var token in StepperCancelTokens)
            {
                if (!token.IsCancellationRequested)
                {
                    token.Cancel();
                }
            }
        }
    }
}

Ça s'appelle comme si ...

private Debouncer StepperDeboucer = new Debouncer(1000); // one second

StepperDeboucer.Debouce(() => { WhateverMethod(args) });

Je ne le recommanderais pas dans les cas où la machine pourrait envoyer des centaines de demandes par seconde, mais pour les entrées de l'utilisateur, cela fonctionne parfaitement. Je l'utilise sur un stepper dans une application Android/IOS qui appelle une api on step.

3
Nieminen

RX est probablement le choix le plus simple, surtout si vous l’utilisez déjà dans votre application. Mais sinon, l'ajouter pourrait être un peu exagéré.

Pour les applications basées sur l'interface utilisateur (comme WPF), j'utilise la classe suivante qui utilise DispatcherTimer:

public class DebounceDispatcher
{
    private DispatcherTimer timer;
    private DateTime timerStarted { get; set; } = DateTime.UtcNow.AddYears(-1);

    public void Debounce(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        // timer is recreated for each event and effectively
        // resets the timeout. Action only fires after timeout has fully
        // elapsed without other events firing in between
        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
    }
}

Pour l'utiliser:

private DebounceDispatcher debounceTimer = new DebounceDispatcher();

private void TextSearchText_KeyUp(object sender, KeyEventArgs e)
{
    debounceTimer.Debounce(500, parm =>
    {
        Model.AppModel.Window.ShowStatus("Searching topics...");
        Model.TopicsFilter = TextSearchText.Text;
        Model.AppModel.Window.ShowStatus();
    });
}

Les événements clés ne sont désormais traités que lorsque le clavier est inactif pendant 200 ms. Tous les événements en attente précédents sont ignorés.

Il existe également une méthode Throttle qui déclenche toujours des événements après un intervalle donné:

    public void Throttle(int interval, Action<object> action,
        object param = null,
        DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
        Dispatcher disp = null)
    {
        // kill pending timer and pending ticks
        timer?.Stop();
        timer = null;

        if (disp == null)
            disp = Dispatcher.CurrentDispatcher;

        var curTime = DateTime.UtcNow;

        // if timeout is not up yet - adjust timeout to fire 
        // with potentially new Action parameters           
        if (curTime.Subtract(timerStarted).TotalMilliseconds < interval)
            interval = (int) curTime.Subtract(timerStarted).TotalMilliseconds;

        timer = new DispatcherTimer(TimeSpan.FromMilliseconds(interval), priority, (s, e) =>
        {
            if (timer == null)
                return;

            timer?.Stop();
            timer = null;
            action.Invoke(param);
        }, disp);

        timer.Start();
        timerStarted = curTime;            
    }
2
Rick Strahl

Rappelez-vous simplement le dernier succès:

DateTime latestHit = DatetIme.MinValue;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");
    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
}

Cela permettra un deuxième événement s'il y avait suffisamment de temps après le dernier événement.

Remarque: il n'est pas possible (de manière simple) de gérer l'événement last dans une série d'événements rapides, car vous ne savez jamais lequel est le last ...

... sauf si vous êtes prêt à gérer le last événement d'une rafale datant de longtemps. Ensuite, vous devez vous souvenir du dernier événement et le consigner si le prochain événement est suffisamment lent:

DateTime latestHit = DatetIme.MinValue;
Machine historicEvent;

private void eventRxVARxH(MachineClass Machine)
{
    log.Debug("Event fired");

    if(latestHit - DateTime.Now < TimeSpan.FromXYZ() // too fast
    {
        // ignore second hit, too fast
        historicEvent = Machine; // or some property
        return;
    }
    latestHit = DateTime.Now;
    // it was slow enough, do processing
    ...
    // process historicEvent
    ...
    historicEvent = Machine; 
}
1
DrKoch

Je sais que j'ai quelques centaines de milliers de minutes de retard dans cette soirée, mais je me suis dit que j'ajouterais mes 2 centimes. Je suis surpris que personne ne l'ait suggéré, donc je suppose qu'il y a quelque chose que je ne sais pas qui pourrait le rendre moins qu'idéal, alors peut-être que j'apprendrai quelque chose de nouveau si cela se fait abattre . J'utilise souvent une solution qui utilise la méthode Change() du System.Threading.Timer.

using System.Threading;

Timer delayedActionTimer;

public MyClass()
{
    // Setup our timer
    delayedActionTimer = new Timer(saveOrWhatever, // The method to call when triggered
                                   null, // State object (Not required)
                                   Timeout.Infinite, // Start disabled
                                   Timeout.Infinite); // Don't repeat the trigger
}

// A change was made that we want to save but not until a
// reasonable amount of time between changes has gone by
// so that we're not saving on every keystroke/trigger event.
public void TextChanged()
{
    delayedActionTimer.Change(3000, // Trigger this timers function in 3 seconds,
                                    // overwriting any existing countdown
                              Timeout.Infinite); // Don't repeat this trigger; Only fire once
}

// Timer requires the method take an Object which we've set to null since we don't
// need it for this example
private void saveOrWhatever(Object obj) 
{
    /*Do the thing*/
}
0
HDL_CinC_Dragon

Je suis venu avec cela dans ma définition de classe.

Je voulais exécuter mon action immédiatement s'il n'y avait aucune action pendant la période (3 secondes dans l'exemple).

Si quelque chose est arrivé dans les trois dernières secondes, je veux envoyer la dernière chose qui s’est produite dans ce délai.

    private Task _debounceTask = Task.CompletedTask;
    private volatile Action _debounceAction;

    /// <summary>
    /// Debounces anything passed through this 
    /// function to happen at most every three seconds
    /// </summary>
    /// <param name="act">An action to run</param>
    private async void DebounceAction(Action act)
    {
        _debounceAction = act;
        await _debounceTask;

        if (_debounceAction == act)
        {
            _debounceTask = Task.Delay(3000);
            act();
        }
    }

Donc, si j'ai subdivisé mon horloge en tous les quarts de seconde

  TIME:  1e&a2e&a3e&a4e&a5e&a6e&a7e&a8e&a9e&a0e&a
  EVENT:  A         B    C   D  E              F  
OBSERVED: A           B           E            F

Notez qu'aucune tentative d'annulation de la tâche n'est effectuée à un stade précoce. Il est donc possible que les actions s'empilent pendant 3 secondes avant d'être éventuellement disponibles pour le nettoyage de la mémoire.

0
Anthony Wieser