C# событие debounce

Я слушаю аппаратное сообщение о событии, но мне нужно отменить его, чтобы избежать слишком большого количества запросов.

Это аппаратное событие, которое отправляет информацию о состоянии компьютера, и мне приходится сохранять его в базе данных для статистических целей, и иногда случается, что его состояние изменяется очень часто (мигает?). В этом случае я бы хотел сохранить только "стабильный" статус и реализовать его, просто подождав 1-2 секунды, прежде чем сохранить статус в базе данных.

Это мой код:

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

Я называю это поведение "debounce": несколько раз подождите, чтобы действительно выполнить свою работу: если одно и то же событие запускается снова в течение времени debounce, я должен отклонить первый запрос и начать ждать время debounce, чтобы завершить второе событие.

Каков наилучший выбор для управления этим? Просто одноразовый таймер?

Чтобы объяснить функцию "debounce", пожалуйста, смотрите эту реализацию javascript для ключевых событий: http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/

23 ответа

Решение

Это не тривиальный запрос на код с нуля, так как есть несколько нюансов. Похожий сценарий - наблюдение за FileSystemWatcher и ожидание того, чтобы что-то успокоилось после большой копии, прежде чем пытаться открыть измененные файлы.

Реактивные расширения в.NET 4.5 были созданы для обработки именно этих сценариев. Вы можете легко использовать их, чтобы обеспечить такую ​​функциональность такими методами, как Throttle, Buffer, Window или Sample. Вы публикуете события для субъекта, применяете к нему одну из оконных функций, например, чтобы получать уведомление только в том случае, если не было никаких действий в течение X секунд или Y событий, а затем подписываетесь на уведомление.

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

Throttle возвращает последнее событие в скользящем окне, только если в этом окне не было других событий. Любое событие сбрасывает окно.

Вы можете найти очень хороший обзор сдвинутых во времени функций здесь

Когда ваш код получает событие, вам нужно только опубликовать его в теме с помощью OnNext:

_mySubject.OnNext(MyEventData);

Если ваше аппаратное событие выглядит как типичное событие.NET, вы можете обойти тему и ручную публикацию с помощью Observable.FromEventPattern, как показано здесь:

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

Вы также можете создавать наблюдаемые из задач, комбинировать последовательности событий с операторами LINQ для запроса, например: пары различных аппаратных событий с Zip, использовать другой источник событий для привязки Throttle/Buffer и т. Д., Добавлять задержки и многое другое.

Reactive Extensions доступны в виде пакета NuGet, поэтому их очень легко добавить в ваш проект.

Книга Стивена Клири " Параллелизм в C# Cookbook", среди прочего, является очень хорошим ресурсом по Reactive Extensions и объясняет, как его использовать и как он сочетается с остальными параллельными API в.NET, такими как Задачи, События и т. Д.

Введение в Rx - это отличная серия статей (откуда я скопировал примеры) с несколькими примерами.

ОБНОВИТЬ

Используя ваш конкретный пример, вы можете сделать что-то вроде:

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

Конечно, это может быть значительно улучшено - как наблюдаемый, так и подписка должны быть уничтожены в какой-то момент. Этот код предполагает, что вы управляете только одним устройством. Если у вас много устройств, вы можете создать наблюдаемое внутри класса, чтобы каждый MachineClass выставлял и располагал свою собственную наблюдаемую.

Я использовал это, чтобы отменить события с некоторым успехом:

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

использование

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

Возможно, он не такой надежный, как в RX, но его легко понять и использовать.

Недавно я занимался техническим обслуживанием приложения, нацеленного на более старую версию.NET Framework (v3.5).

Я не мог использовать Reactive Extensions или Task Parallel Library, но мне нужен был хороший, чистый, последовательный способ отменить события. Вот что я придумал:

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

Вот пример использования его в форме окна с текстовым полем поиска:

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

Я столкнулся с проблемами с этим. Я попробовал каждый из ответов здесь, и, поскольку я нахожусь в универсальном приложении Xamarin, мне кажется, что мне не хватает определенных вещей, которые требуются в каждом из этих ответов, и я не хотел добавлять больше пакетов или библиотек. Мое решение работает именно так, как я ожидал, и у меня не возникало никаких проблем с ним. Надеюсь, это кому-нибудь поможет.

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

Это называется так...

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

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

Я не рекомендовал бы это ни для чего, где машина могла бы посылать сотни запросов в секунду, но для пользовательского ввода это работает превосходно. Я использую его на степпере в приложении для Android/IOS, которое вызывает API на шаге.

RX, вероятно, самый простой выбор, особенно если вы уже используете его в своем приложении. Но если нет, добавление может быть немного излишним.

Для приложений на основе пользовательского интерфейса (например, WPF) я использую следующий класс, который использует 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();
    }
}

Чтобы использовать это:

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

Ключевые события теперь обрабатываются только после того, как клавиатура простаивает в течение 200 мс - все предыдущие ожидающие события отбрасываются.

Есть также метод Throttle, который всегда запускает события после заданного интервала:

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

Мне нужно было что-то подобное, но в веб-приложении, поэтому я не могу хранить Action в переменной она будет потеряна между HTTP-запросами.

Основываясь на других ответах и ​​идее @Collie, я создал класс, который смотрит на уникальный строковый ключ для регулирования.

public static class Debouncer
{
    static ConcurrentDictionary<string, CancellationTokenSource> _tokens = new ConcurrentDictionary<string, CancellationTokenSource>();
    public static void Debounce(string uniqueKey, Action action, int seconds)
    {
        var token = _tokens.AddOrUpdate(uniqueKey,
            (key) => //key not found - create new
            {
                return new CancellationTokenSource();
            },
            (key, existingToken) => //key found - cancel task and recreate
            {
                existingToken.Cancel(); //cancel previous
                return new CancellationTokenSource();
            }
        );

        Task.Delay(seconds * 1000, token.Token).ContinueWith(task =>
        {
            if (!task.IsCanceled)
            {
                action();
                _tokens.TryRemove(uniqueKey, out _);
            }
        }, token.Token);
    }
}

Применение:

//throttle for 5 secs if it's already been called with this KEY
Debouncer.Debounce("Some-Unique-ID", () => SendEmails(), 5);

В качестве дополнительного бонуса, поскольку он основан на строковом ключе, вы можете использовать встроенные лямбда-выражения

Debouncer.Debounce("Some-Unique-ID", () => 
{
    //do some work here
}, 5);

Этот маленький драгоценный камень вдохновлен Майк Wards дьявольски изобретательной удлинительной попытки. Тем не менее, этот довольно хорошо убирает после себя.

public static Action Debounce(this Action action, int milliseconds = 300)
{
    CancellationTokenSource lastCToken = null;

    return () =>
    {
        //Cancel/dispose previous
        lastCToken?.Cancel();
        try { 
            lastCToken?.Dispose(); 
        } catch {}          

        var tokenSrc = lastCToken = new CancellationTokenSource();

        Task.Delay(milliseconds).ContinueWith(task => { action(); }, tokenSrc.Token);
    };
}

Примечание: в этом случае нет необходимости избавляться от задачи. См. Здесь доказательства.

Применение

Action DebounceToConsole;
int count = 0;

void Main()
{
    //Assign
    DebounceToConsole = ((Action)ToConsole).Debounce(50);

    var random = new Random();
    for (int i = 0; i < 50; i++)
    {
        DebounceToConsole();
        Thread.Sleep(random.Next(100));
    }
}

public void ToConsole()
{
    Console.WriteLine($"I ran for the {++count} time.");
}

Ответ Панагиотиса, безусловно, правильный, однако я хотел привести более простой пример, поскольку мне потребовалось некоторое время, чтобы разобраться, как заставить его работать. Мой сценарий заключается в том, что пользователь вводит данные в поле поиска, и по мере того, как пользователь вводит данные, мы хотим сделать вызовы API для возврата подсказок поиска, поэтому мы хотим отменить вызовы API, чтобы они не делали их каждый раз при вводе символа.

Я использую Xamarin.Android, однако это должно относиться к любому сценарию 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 ();
        }

Когда вы сначала инициализируете экран / класс, вы создаете событие для прослушивания ввода текста пользователем (SearchTextChanged), а затем также настраиваете регулирующую подписку, которая связана с "typingSubject".

Затем в вашем событии SearchTextChanged вы можете вызвать typingSubject.OnNext и передать текст в поле поиска. По истечении периода ожидания (1 секунда) он вызовет подписанное событие (предположения, Adapter.Get в нашем случае.)

Наконец, когда экран закрыт, не забудьте избавиться от подписки!

Создал этот класс для его решения также для ожидаемых вызовов:

      public class Debouncer
{
    private CancellationTokenSource _cancelTokenSource = null;

    public async Task Debounce(Func<Task> method, int milliseconds = 300)
    {
        _cancelTokenSource?.Cancel();
        _cancelTokenSource?.Dispose();

        _cancelTokenSource = new CancellationTokenSource();

        await Task.Delay(milliseconds, _cancelTokenSource.Token);

        await method();
    }
}

Пример использования:

      private Debouncer _debouncer = new Debouncer();
....
await _debouncer.Debounce(YourAwaitableMethod);

Это вдохновляет Task.Delay на основе Ниеминен в классе Debouncer. Упрощено, некоторые мелкие исправления и должны убрать после себя лучше.

class Debouncer: IDisposable
{
    private CancellationTokenSource lastCToken;
    private int milliseconds;

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

    public void Debounce(Action action)
    {
        Cancel(lastCToken);

        var tokenSrc = lastCToken = new CancellationTokenSource();

        Task.Delay(milliseconds).ContinueWith(task =>
        {
             action();
        }, 
            tokenSrc.Token
        );
    }

    public void Cancel(CancellationTokenSource source)
    {
        if (source != null)
        {
            source.Cancel();
            source.Dispose();
        }                 
    }

    public void Dispose()
    {
        Cancel(lastCToken);
    }

    ~Debouncer()
    {
        Dispose();
    }
}

Применение

private Debouncer debouncer = new Debouncer(500); //1/2 a second
...
debouncer.Debounce(SomeAction);

Мне нужен был метод Debounce для Blazor, и я постоянно возвращался на эту страницу, поэтому я хотел поделиться своим решением на случай, если оно поможет другим.

public class DebounceHelper
{
    private CancellationTokenSource debounceToken = null;

    public async Task DebounceAsync(Func<CancellationToken, Task> func, int milliseconds = 1000)
    {
        try
        {
            // Cancel previous task
            if (debounceToken != null) { debounceToken.Cancel(); }

            // Assign new token
            debounceToken = new CancellationTokenSource();

            // Debounce delay
            await Task.Delay(milliseconds, debounceToken.Token);

            // Throw if canceled
            debounceToken.Token.ThrowIfCancellationRequested();

            // Run function
            await func(debounceToken.Token);
        }
        catch (TaskCanceledException) { }
    }
}

Пример вызова функции поиска

<input type="text" @oninput=@(async (eventArgs) => await OnSearchInput(eventArgs)) />

@code {
    private readonly DebounceHelper debouncer = new DebounceHelper();

    private async Task OnSearchInput(ChangeEventArgs eventArgs)
    {
        await debouncer.DebounceAsync(async (cancellationToken) =>
        {
            // Search Code Here         
        });
    }
}

Я сделал более простое решение, основанное на ответе @Mike Ward:

      public static class CustomTaskExtension
{
    #region fields

    private static int _last = 0;

    #endregion

    public static void Debounce(CancellationTokenSource throttleCts, double debounceTimeMs, Action action)
    {
        var current = Interlocked.Increment(ref _last);
        Task.Delay(TimeSpan.FromMilliseconds(debounceTimeMs), throttleCts.Token)
            .ContinueWith(task =>
            {
                if (current == _last) action();
                task.Dispose();
            });
    }
}

Пример использования:

      // security way to cancel the debounce process any time
CancellationTokenSource _throttleCts = new CancellationTokenSource();

public void MethodCalledManyTimes()
{
    // will wait 250ms after the last call
    CustomTaskExtension.Debounce(_throttleCts, 250, async () =>
    {
        Console.Write("Execute your code 250ms after the last call.");
    });
}

Просто запомните последний хит:

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

Это позволит второе событие, если было достаточно времени после последнего события.

Обратите внимание: невозможно (простым способом) обработать последнее событие в серии быстрых событий, потому что вы никогда не знаете, какое из них является последним...

... если вы не готовы справиться с последним событием всплеска, который произошел давно. Затем вы должны запомнить последнее событие и записать его, если следующее событие достаточно медленное:

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

Другая реализация

      public static class Debounce
{
    public static Action Action(Action action, TimeSpan time)
    {
        var timer = new Timer(_ => action(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);

        return () => timer.Change(time, Timeout.InfiniteTimeSpan);
    }
}

Я написал асинхронный отладчик, который не запускает асинхронно.

      public sealed class Debouncer : IDisposable {

  public Debouncer(TimeSpan? delay) => _delay = delay ?? TimeSpan.FromSeconds(2);

  private readonly TimeSpan _delay;
  private CancellationTokenSource? previousCancellationToken = null;

  public async Task Debounce(Action action) {
    _ = action ?? throw new ArgumentNullException(nameof(action));
    Cancel();
    previousCancellationToken = new CancellationTokenSource();
    try {
      await Task.Delay(_delay, previousCancellationToken.Token);
      await Task.Run(action, previousCancellationToken.Token);
    }
    catch (TaskCanceledException) { }    // can swallow exception as nothing more to do if task cancelled
  }

  public void Cancel() {
    if (previousCancellationToken != null) {
      previousCancellationToken.Cancel();
      previousCancellationToken.Dispose();
    }
  }

  public void Dispose() => Cancel();

}

Я использую его, чтобы отменить изменения, сообщаемые при изменениях файлов, см. Полный пример здесь .

Меня вдохновил ответ Майка, но мне нужно было решение, которое работало бы без задач, которое просто проглатывает последующие вызовы событий, пока не истечет время ожидания отладки. Вот мое решение:

      public static Action<T> Debounce<T>(this Action<T> action, int milliseconds = 300)
{
    DateTime? runningCallTime = null;
    var locker = new object();

    return arg =>
    {
        lock (locker)
        {
            if (!runningCallTime.HasValue ||
                runningCallTime.Value.AddMilliseconds(milliseconds) <= DateTime.UtcNow)
            {
                runningCallTime = DateTime.UtcNow;
                action.Invoke(arg);
            }
        }
    };

}

Я придумал это в своем определении класса.

Я хотел немедленно запустить свое действие, если в течение периода времени не было никаких действий (в примере 3 секунды).

Если что-то произошло за последние три секунды, я хочу отправить последнее, что произошло за это время.

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

Итак, если я делю свои часы на каждую четверть секунды

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

Обратите внимание, что не делается никаких попыток отменить задачу раньше, поэтому действия могут накапливаться в течение 3 секунд, прежде чем они станут доступны для сбора мусора.

Ни один из приведенных выше ответов не работал у меня полностью, поэтому я придумал следующую реализацию:

      public class Debouncer
{
    private CancellationTokenSource _cancelTokenSource = null;

    public Task Debounce(Func<Task> method, int milliseconds = 250)
    {
        _cancelTokenSource?.Cancel();
        _cancelTokenSource?.Dispose();
        _cancelTokenSource = new CancellationTokenSource();

        try
        {
            return Task.Delay(milliseconds, _cancelTokenSource.Token)
                .ContinueWith(_ => method(), _cancelTokenSource.Token);
        }
        catch (TaskCanceledException exception) when (exception.CancellationToken == _cancelTokenSource.Token)
        {
        }
        
        return Task.CompletedTask;
    }
}

Использование:

      var debouncer = new Debouncer();
await debouncer.Debounce(async () => await someAction());

Я использую эту реализацию для приложений GUI/WinForms.

Он предназначен для сохранения WeakReference на цель вызова, чтобы избежать утечек памяти. Try/catch намеренно игнорируетObjectDisposedExceptionпоскольку логично, что вызов метода больше не актуален.

      public static class ThrottledActionFactory {
    public static readonly TimeSpan DEFAULT_THROTTLE_TIME = TimeSpan.FromMilliseconds(300);

    public static Action Create(this Action action, TimeSpan? time = null, TaskScheduler? scheduler = null) {
        var throttledWrapper = CreateThrottledAction(action.Target, action.Method, time, scheduler);
        return () => throttledWrapper(new object?[] { });
    }

    public static Action<T> Create<T>(this Action<T> action, TimeSpan? time = null, TaskScheduler? scheduler = null) {
        var throttledWrapper = CreateThrottledAction(action.Target, action.Method, time, scheduler);
        return arg => throttledWrapper(new object?[] {arg});
    }

    public static Action<T1, T2> Create<T1, T2>(this Action<T1, T2> action, TimeSpan? time = null, TaskScheduler? scheduler = null) {
        var throttledAction = CreateThrottledAction(action.Target, action.Method, time, scheduler);
        return (arg1, arg2) => throttledAction(new object?[] {arg1, arg2});
    }

    private static Action<object?[]> CreateThrottledAction(object target, MethodInfo method, TimeSpan? time, TaskScheduler? scheduler) {
        var targetReference = new WeakReference<object>(target);
        return CreateThrottledAction(targetReference, method, time ?? DEFAULT_THROTTLE_TIME, scheduler ?? TaskScheduler.Default);
    }

    private static Action<object?[]> CreateThrottledAction(WeakReference<object> targetReference, MethodInfo method, TimeSpan time, TaskScheduler scheduler) {
        CancellationTokenSource? cts = null;
        return args => {
            cts?.Cancel();
            cts = new();
            Task.Delay(time, cts.Token)
               .ContinueWith(delayTask => {
                    if (delayTask.IsCanceled || cts.IsCancellationRequested) {
                        return;
                    }

                    if (!targetReference.TryGetTarget(out var target)) {
                        return;
                    }

                    try {
                        method.Invoke(target, args);
                    } catch (ObjectDisposedException) {
                        // ignored
                    }
                }, scheduler);
        };
    }
}

Наконец, можно передать TaskScheduler фабричной функции, чтобы продолжить выполнение в основном потоке.

      public class MyForm : Form {

    ...

    private readonly Action<string> LogConsole = ThrottledActionFactory.Create<string>(msg => {
        Console.WriteLine(msg);
    }, scheduler: TaskScheduler.FromCurrentSynchronizationContext());

    private void OnMouseMove(object sender, EventArgs args) {
        // executes on main thread because of TaskScheduler.FromCurrentSynchronizationContext()
        LogConsole("My Message"); 
    }
}

Выяснили, как использовать пакет System.Reactive NuGet для правильного устранения ошибок в TextBox.

На уровне класса у нас есть поле

private IObservable<EventPattern<TextChangedEventArgs>> textChanged;

Затем, когда мы хотим начать прослушивание события:

// Debouncing capability
textChanged = Observable.FromEventPattern<TextChangedEventArgs>(txtSearch, "TextChanged");
textChanged.ObserveOnDispatcher().Throttle(TimeSpan.FromSeconds(1)).Subscribe(args => {
    Debug.WriteLine("bounce!");
});

Убедитесь, что вы также не подключили текстовое поле к обработчику событий. Вышеупомянутая лямбда - это обработчик событий.

Я использую это для предотвращения дребезга при подключении и отключении USB-устройства. Вы можете использовать это для своих нужд.

      public class Debouncer : IDisposable
{
    readonly TimeSpan _delay;
    readonly Action _action;
    private CancellationTokenSource _lastToken;
    private static int _lastCount = 0;

    public Debouncer(TimeSpan delay, Action action)
    {
        if (delay.Milliseconds < 300)
        {
            delay = TimeSpan.FromMilliseconds(300); // minimum debounce delay
        }
        _delay = delay;
        _action = action;
    }

    public void Invoke()
    {
        try
        {
            if (_lastToken != null)
            {
                Cancel(_lastToken);
            }

            var currentCount = Interlocked.Increment(ref _lastCount);
            var cancelToken = _lastToken = new CancellationTokenSource();

            Task.Delay(_delay).ContinueWith(task =>
            {
                if (currentCount == _lastCount)
                {
                    _action();
                }
            }, cancelToken.Token);
        }
        catch (Exception ex)
        {
            Log.Exception(ex);
        }
    }
    public void Cancel(CancellationTokenSource token)
    {
        try
        {
            if (token != null)
            {
                token.Cancel();
                token.Dispose();
            }
        }
        catch (Exception ex)
        {
            Log.Exception(ex);
        }
    }
    public void Dispose()
    {
        try
        {
            Cancel(_lastToken);
        }
        catch (Exception ex)
        {
            Log.Exception(ex);
        }
    }
    ~Debouncer()
    {
        Dispose();
    }
}

Как использовать вышеуказанный дебаунсер?

      protected override void WndProc(ref Message m)
{
    // detect when usb device (reader, printer) is plug or unplug
    try
    {
        base.WndProc(ref m);
        if (m.Msg == UsbNotify.WM_DEVICECHANGE)
        {
            switch ((int)m.WParam)
            {
                case UsbNotify.DBT_DEVICEREMOVECOMPLETE:
                    new Debouncer(TimeSpan.FromMilliseconds(300), UsbDetached).Invoke();
                    break;

                case UsbNotify.DBT_DEVICEARRIVAL:
                    new Debouncer(TimeSpan.FromMilliseconds(300), UsbAttached).Invoke();
                    break;
            }
        }
    }
    catch (Exception ex)
    {
        Log.Exception(ex);
    }
}

Принимает функцию агрегирования для создания агрегированных входных данных и, наконец, использует эти агрегированные входные данные в результирующей задаче. Несколько классов могут ожидать выполнения одной задачи, чтобы несколько потоков ожидали результата одной операции.

      public static class Debouncer
    {
        static object lockObject = new object();
        static ConcurrentDictionary<string, Tuple<Task, DateTime, object>> _taskTimeInputDictionary = new();
        public static async Task<T> Debounce<inputT, T>(string uniqueKey, int milliseconds, Func<inputT,inputT> inputAggregator, Func<inputT,T> action)
        {
            Task<T> task = null;
            lock (lockObject)
            {
                task = (Task<T>)(_taskTimeInputDictionary.AddOrUpdate(uniqueKey,
                    (key) => //key not found - create new
                    {
                        return new Tuple<Task, DateTime, object>(Task.Run(async () =>
                        {
                            while (true)
                            {
                                object input;
                                bool Run;
                                lock (lockObject) {
                                    var executeTime = _taskTimeInputDictionary[uniqueKey].Item2;
                                    input = _taskTimeInputDictionary[uniqueKey].Item3;
                                    Run = DateTime.Now > executeTime;
                                    if (Run)
                                    {
                                        _taskTimeInputDictionary.Remove(uniqueKey, out Tuple<Task, DateTime, object> _);
                                    }
                                }
                                if (Run) 
                                    return action((inputT) input);
                                else await Task.Yield();
                            }
                        }), DateTime.Now.AddMilliseconds(milliseconds), inputAggregator(default(inputT)));
                    },
                    (key, tt) =>
                    {
                        return new Tuple<Task, DateTime, object>(tt.Item1, DateTime.Now.AddMilliseconds(milliseconds), inputAggregator((inputT)tt.Item3));
                    }
                ).Item1);
            }
            return await task;
        }
    }

Объект блокировки используется только потому, что по какой-то причине ConcurrentDictionary не выполняет должным образом задачу по предотвращению одновременного многократного добавления одного и того же ключа. Таким образом, блокируя объект, мы устраняем любую вероятность возникновения проблем, связанных со словарем. Улучшением этого может быть наличие нескольких объектов блокировки для каждого уникального ключа, чтобы не блокировать уникальные ключи.

Я знаю, что опоздал на эту вечеринку на пару сотен тысяч минут, но я решил добавить свои 2 цента. Я удивлен, что никто не предложил это, поэтому я предполагаю, что есть кое-что, что я не знаю, что могло бы сделать это не идеальным, поэтому, возможно, я узнаю что-то новое, если это будет сбито. Я часто использую решение, которое использует System.Threading.Timer"s Change() метод.

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*/
}
Другие вопросы по тегам