Как избежать тупиковых ситуаций с помощью Observable FromEventPattern Async? - PullRequest
0 голосов
/ 03 января 2019

Я использую наблюдаемые / реактивные расширения, чтобы отменить определенные события, такие как нажатия кнопок или ввод текста в текстовое поле.Однако в случае выключения или закрытия мне нужно дождаться любых ожидающих событий, чтобы можно было завершить операции сохранения и т. Д.

Следующий код заблокируется.

Button b1 = new Button();

var scheduler = new EventLoopScheduler(ts => new Thread(ts)
{
    IsBackground = false
});

var awaiter = Observable.FromEventPattern(h => b1.Click += h, h => b1.Click -= h, scheduler)                
     .Throttle(TimeSpan.FromMilliseconds(5000), scheduler)
     .FirstOrDefaultAsync();

someTaskList.add(awaiter.ToTask());

awaiter.Subscribe
(
    x =>
    {
        //do some work in response to click event
    }
);

//program continues...

Затем,в другом месте приложения

private async Task CloseApplicationSafely()
{
    await AwaitPendingEvents();
}

private async Task AwaitPendingEvents()
{
    if(someTaskList.Count > 0)
    {
        await Task.WhenAll(someTaskList);
    }
}

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

var completedTask = Observable.FromEventPattern(h => t1.TextChanged += h, h => t1.TextChanged -= h, scheduler)
    .Select(x => ((TextBox)x.Sender).Text)
    .DistinctUntilChanged()
    .Throttle(TimeSpan.FromMilliseconds(5000), scheduler)
    .ForEachAsync(txt =>
    {
        //do some work, save the text
    });

someTaskList.Add(completedTask);

В этом случае не имеет значения, был ли текст когда-либо изменен или нет.Переменная completeTask навсегда заблокируется, если вы ее дождетесь.ForEachAsync () возвращает задачу, которая, кажется, никогда не активируется.

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

1 Ответ

0 голосов
/ 04 января 2019

Комментарии от @Servy и @Enigmativity помогли мне определить это.Для тех, кто заинтересован, вот решение, которое я придумал.Любые предложения по моему подходу дают мне знать.

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

public static Task WaitableDebouncer(
    this Control c, 
    Action<EventHandler> addHandler, 
    Action<EventHandler> removeHandler, 
    IScheduler scheduler,
    CancellationToken cancelToken,
    TimeSpan limit,
    Func<Task> func)
{
    var mycts = new CancellationTokenSource();

    bool activated = false;
    bool active = false;

    Func<Task> pending = null;

    var awaiter = Observable.FromEventPattern(addHandler, removeHandler, scheduler)
        .TakeUntil(x => { return cancelToken.IsCancellationRequested; })
        .Do((x) => { activated = true; })
        .Do((x) =>
        {
            //sets pending task to last in sequence
            pending = func;
        })
        .Throttle(limit, scheduler)
        .Do((x) => { active = true; })    //done with throttle
        .ForEachAsync(async (x) =>
        {
            //get func
            var f = pending;

            //remove from list
            pending = null;

            //execute it
            await f();

            //have we been cancelled?
            if (cancelToken.IsCancellationRequested)
            {
                mycts.Cancel();
            }

            //not active
            active = false;

        }, mycts.Token);

    //if cancelled 
    cancelToken.Register(() => 
    {
        //never activated, force cancel
        if (!activated)
        {
            mycts.Cancel();
        }

        //activated in the past but not currently active
        if (activated && !active)
        {
            mycts.Cancel();
        }
    });

    //return new awaiter based on conditions
    return Task.Run(async () =>
    {
        try
        {
            //until awaiter finishes or is cancelled, this will block
            await awaiter;
        }
        catch (Exception)
        {
            //cancelled, don't care
        }

        //if pending isn't null, that means we terminated before ForEachAsync reached it
        //execute it
        if (pending != null)
        {
            await pending();
        }
    });
}

Затем я использую его следующим образом.Вот пример с нажатием кнопки, b1 - объект System.Windows.Forms.Button.Это может быть что угодно.Для моего тестового приложения я менял цвета на некоторых панелях в главной форме.В соответствии с предыдущим кодом в OP задачи - это просто список типа Task.

var awaiter1 = b1.WaitableDebouncer(h => b1.Click += h, h => b1.Click -= h, 
    scheduler, 
    canceller.Token, 
    TimeSpan.FromMilliseconds(5000), 
    async () =>
    {
        Invoke(new Action(() =>
        {
            if (p1.BackColor == Color.Red)
            {
                p1.BackColor = Color.Orange;
            }
            else if (p1.BackColor == Color.Orange)
            {
                p1.BackColor = Color.Yellow;
            }
            else if (p1.BackColor == Color.Yellow)
            {
                p1.BackColor = Color.HotPink;
            }
            else
            {
                p1.BackColor = Color.Red;
            }
        }));
    });

tasks.Add(awaiter1);

Еще один для TextChanged для текстового поля.t1 - это System.Windows.Forms.TextBox.Опять же, это может быть что угодно, я просто устанавливаю статическую строковую переменную someValue и обновляю метку в пользовательском интерфейсе.

var awaiter2 = t1.WaitableDebouncer(h => t1.TextChanged += h, h => t1.TextChanged -= h, 
    scheduler, 
    canceller.Token, 
    TimeSpan.FromMilliseconds(5000), 
    async () =>
    {
        savedValue = t1.Text;

        Invoke(new Action(() => l1.Text = savedValue));
    });

tasks.Add(awaiter2);  

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

private async Task AwaitPendingEvents()
{
    if (tasks.Count > 0)
    {
        await Task.WhenAll(tasks);
    }            
}

У нас есть программа ожидания для всего приложения.При закрытии мы делаем.

//main cancel signal
canceller.Cancel();

await AwaitPendingEvents();

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

...