Дросселируйте, но отбросьте результаты, если они пришли слишком поздно

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

Моей первой мыслью было то, что примитивная дроссельная заслонка Rx была идеальным вариантом, но она принесла мне половину результата.

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

Проблема в том, что я хочу отбросить / пропустить / выбросить результат, если пользователь снова наберет интервал времени дроссельной заслонки.

Например:

  • Время начинается, и пользователь нажимает клавишу: 0 мс
  • Дроссельная заслонка установлена ​​на 100 мс.
  • Получение занимает 200 мс.
  • Через 150 мс пользователь нажал другую клавишу

Теперь с помощью Throttle первая выборка по-прежнему будет продолжаться, чтобы заполнить список предложений графического интерфейса. Мне нравится узнавать, как я могу отменить эту первую выборку, если она больше не актуальна? Только второе нажатие клавиши должно вызвать обновление графического интерфейса.

Вот что я пробовал

(Я использую ReactiveUI, но вопрос о Rx)

public IEnumerable<usp_getOpdrachtgevers_Result> Results { get; set; } // [Reactive] pu

public SearchOpdrachtgeverVM()
{

    this.WhenAnyValue(x => x.FirstName,
                      x => x.LastName
        )
        .Throttle(TimeSpan.FromMilliseconds(200))
        .Subscribe(async vm => Results = await PopulateGrid());
}

private async Task<IEnumerable<usp_getOpdrachtgevers_Result>> PopulateGrid()
{

    return await Task.Run(
             () => _opdrachtgeversCache
                         .Where(x =>
                                x.vNaam.Contains(FirstName)
                                && x.vLastName.Contains(LastName)
                         )

             );

}

person buckley    schedule 25.08.2016    source источник


Ответы (2)


Если вы превратите свою асинхронную задачу в Observable, это будет выглядеть как классическое использование Switch:

this.WhenAnyValue(x => x.FirstName,
                  x => x.LastName
    )
    .Throttle(TimeSpan.FromMilliseconds(100)) 
    .Select(l => PopulateGrid().ToObservable())
    .Switch()
    .Subscribe(vm => Results = vm);

Throttle следует использовать для подавления вызовов, пока пользователь печатает. Так что отрегулируйте этот TimeSpan по своему усмотрению.

person Shlomo    schedule 25.08.2016
comment
Ааа переключатель да. Это примитив, который может быть полезен, когда я прочитаю о нем, и он мне подходит. Я попробую завтра. Определение switch: преобразует наблюдаемую последовательность наблюдаемых последовательностей в наблюдаемую последовательность, производящую значения только из самой последней наблюдаемой последовательности. - person buckley; 25.08.2016

Если я правильно понимаю, что вы хотите, это можно сделать довольно просто и очистить, если немного реорганизовать свой код.

Во-первых, сделайте триггеры имени и фамилии наблюдаемыми. В приведенном ниже коде я использовал субъекты, но будет лучше, если вы сможете использовать статические методы Observable для «преобразования» их в наблюдаемые; например Observable.FromEvent.

Затем превратите код для получения результатов в наблюдаемое. В приведенном ниже коде я использовал Observable.Create, чтобы вернуть поток IEnumerable<string>.

Наконец, вы можете использовать оператор Switch, чтобы подписаться на каждый новый вызов GetResults и отменить предыдущий вызов GetResults.

Звучит сложно, но код довольно прост:

private Subject<string> _firstName = new Subject<string>();
private Subject<string> _lastName = new Subject<string>();

private Task<IEnumerable<string>> FetchResults(string firstName, string lastName, CancellationToken cancellationToken)
{
    // Fetch the results, respecting the cancellation token at the earliest opportunity
    return Task.FromResult(Enumerable.Empty<string>());
}

private IObservable<IEnumerable<string>> GetResults(string firstName, string lastName)
{
    return Observable.Create<IEnumerable<string>>(
        async observer =>
        {
            // Use a cancellation disposable to provide a cancellation token the the asynchronous method
            // When the subscription to this observable is disposed, the cancellation token will be cancelled.
            CancellationDisposable disposable = new CancellationDisposable();

            IEnumerable<string> results = await FetchResults(firstName, lastName, disposable.Token);

            if (!disposable.IsDisposed)
            {
                observer.OnNext(results);
                observer.OnCompleted();
            }

            return disposable;
        }
    );
}

private void UpdateGrid(IEnumerable<string> results)
{
    // Do whatever
}

private IDisposable ShouldUpdateGridWhenFirstOrLastNameChanges()
{
    return Observable
        // Whenever the first or last name changes, create a tuple of the first and last name
        .CombineLatest(_firstName, _lastName, (firstName, lastName) => new { FirstName = firstName, LastName = lastName })
        // Throttle these tuples so we only get a value after it has settled for 100ms
        .Throttle(TimeSpan.FromMilliseconds(100))
        // Select the results as an observable
        .Select(tuple => GetResults(tuple.FirstName, tuple.LastName))
        // Subscribe to the new results and cancel any previous subscription
        .Switch()
        // Use the new results to update the grid
        .Subscribe(UpdateGrid);
}

Подсказка: вам действительно стоит передать явный планировщик в Throttle, чтобы вы могли эффективно тестировать этот код с помощью TestScheduler.

Надеюсь, поможет.

person ibebbs    schedule 25.08.2016
comment
Метод WhenAnyValue - это сокращение ReactiveUI для всего того тематического кода, который вы предлагаете. - person Shlomo; 25.08.2016
comment
Привет @ibebbs. Как заметил Шломо, я использую ReactiveUI, чтобы избавиться от некоторых проблем. Я очень ценю ваш ответ, написав его! И совет о планировщике - это то, что я тоже рассмотрю. - person buckley; 25.08.2016
comment
Похоже, это пример учебника introtorx.com/content /v1.0.10621.0/ - person buckley; 25.08.2016
comment
Из reactivex.io/documentation/operators/switch.html Это тонкое поведение кажется важным: Обратите внимание, что он откажется от подписки на ранее отправленный Observable, когда новый Observable испускается из исходного Observable, а не когда новый Observable испускает элемент. Реализация также может заключаться в том, что отмена подписки происходит после отправки элемента (quod non). Выбранное поведение - это то, что необходимо в этом сценарии. - person buckley; 25.08.2016