Skip to content

C# Ereignis-Entprellung

Unser Expertenteam hat nach vielen Tagen der Arbeit und Datenerfassung die erforderlichen Daten erhalten, unser Wunsch ist, dass sie für Ihre Arbeit nützlich sind.

Lösung:

Dies ist keine triviale Anforderung, die von Grund auf neu zu codieren ist, da es mehrere Nuancen gibt. Ein ähnliches Szenario ist die Überwachung eines FileSystemWatcher und das Warten darauf, dass sich die Dinge nach einer großen Kopie beruhigen, bevor man versucht, die geänderten Dateien zu öffnen.

Die Reactive Extensions in .NET 4.5 wurden entwickelt, um genau diese Szenarien zu behandeln. Sie können sie leicht verwenden, um solche Funktionalität mit Methoden wie Throttle, Buffer, Window oder Sample bereitzustellen. Man sendet die Ereignisse an ein Subjekt, wendet eine der Windowing-Funktionen darauf an, zum Beispiel um eine Benachrichtigung nur dann zu erhalten, wenn es X Sekunden lang keine Aktivität oder Y Ereignisse gab, und abonniert dann die Benachrichtigung.

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

Throttle gibt das letzte Ereignis in einem gleitenden Fenster nur dann zurück, wenn es keine anderen Ereignisse in diesem Fenster gab. Jedes Ereignis setzt das Fenster zurück.

Eine sehr gute Übersicht über die zeitversetzten Funktionen finden Sie hier

Wenn Ihr Code das Ereignis empfängt, müssen Sie es nur mit OnNext an das Subjekt weiterleiten:

_mySubject.OnNext(MyEventData);

Wenn Ihr Hardware-Ereignis als typisches .NET-Ereignis auftritt, können Sie das Subject und das manuelle Posten mit Observable.FromEventPattern umgehen, wie hier gezeigt:

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

Sie können auch Observables aus Tasks erstellen, Event-Sequenzen mit LINQ-Operatoren kombinieren, um z.B. Paare verschiedener Hardware-Events mit Zip abzufragen, eine andere Event-Quelle zum Binden von Throttle/Buffer etc. verwenden, Verzögerungen hinzufügen und vieles mehr.

Reactive Extensions ist als NuGet-Paket verfügbar, so dass es sehr einfach ist, sie zu Ihrem Projekt hinzuzufügen.

Stephen Clearys Buch "Concurrency in C# Cookbook" ist ein sehr gute Ressource über Reactive Extensions unter anderem, und erklärt, wie man es verwenden kann und wie es mit dem Rest der gleichzeitigen APIs in .NET wie Tasks, Events etc. zusammenpasst.

Introduction to Rx ist eine ausgezeichnete Artikelserie (von dort habe ich die Beispiele kopiert), mit mehreren Beispielen.

UPDATE

Anhand deines konkreten Beispiels könntest du etwa so vorgehen:

IObservable _myObservable;

private MachineClass connect()
{

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

Dies lässt sich natürlich erheblich verbessern - sowohl das Observable als auch das Abonnement müssen zu einem bestimmten Zeitpunkt entsorgt werden. Dieser Code geht davon aus, dass Sie nur ein einziges Gerät kontrollieren. Wenn Sie viele Geräte haben, könnten Sie die Observable innerhalb der Klasse erstellen, so dass jede MachineClass ihre eigene Observable exponiert und entsorgt.

Ich habe dies verwendet, um Ereignisse mit einigem Erfolg zu entprellen:

public static Action Debounce(this Action 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();
        });
    };
}

Verwendung

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

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

Es ist vielleicht nicht so robust wie das, was in RX ist, aber es ist einfach zu verstehen und zu benutzen.

Follow-up 2020-02-03

Die Lösung von @collie mit Storno-Token wurde wie folgt überarbeitet

public static Action Debounce(this Action func, int milliseconds = 300)
{
    CancellationTokenSource? cancelTokenSource = null;

    return arg =>
    {
        cancelTokenSource?.Cancel();
        cancelTokenSource = new CancellationTokenSource();

        Task.Delay(milliseconds, cancelTokenSource.Token)
            .ContinueWith(t =>
            {
                if (t.IsCompletedSuccessfully)
                {
                    func(arg);
                }
            }, TaskScheduler.Default);
    };
}

Anmerkungen:

  • Aufruf von . Cancel reicht aus, um das CTS zu entsorgen.
  • Ein erfolgreich abgeschlossenes CTS wird bis zum nächsten Aufruf nicht abgebrochen/entsorgt
  • Wie von @collie angemerkt, werden Aufgaben entsorgt, so dass kein Aufruf von Dispose für die Aufgabe

Ich habe noch nie mit Storno-Tokens gearbeitet und verwende sie vielleicht nicht richtig.

Kürzlich habe ich einige Wartungsarbeiten an einer Anwendung durchgeführt, die auf eine ältere Version des .NET-Frameworks (v3.5) ausgerichtet war.

Ich konnte weder die Reactive Extensions noch die Task Parallel Library verwenden, aber ich brauchte eine schöne, saubere und konsistente Methode zur Entprellung von Ereignissen. Hier ist, was ich mir ausgedacht habe:

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 _resets = new HashSet();
        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();
                }
            }
        }
    }
}

Hier ist ein Beispiel für die Verwendung in einem Windows-Formular, das ein Suchtextfeld enthält:

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
        }
    }
}

Bewertungen und Kommentare

Denken Sie daran, dass Sie diesen Aufsatz empfehlen können, wenn er Ihr Problem gelöst hat.



Nutzen Sie unsere Suchmaschine

Suche
Generic filters

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.