Проблема объединения запросов Reactive Framework (Rx) для обеспечения правильного поведения пользовательского интерфейса - PullRequest
0 голосов
/ 19 октября 2010

Я пытаюсь удалить более традиционные обработчики событий из приложения Silverlight в пользу использования ряда запросов Rx для обеспечения лучшей, более простой в управлении, поведенческой абстракции.

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

  • У меня есть текстовое поле , где пользователь может ввести текст поиска .
  • Если естьЕсли нет текста (или только пробелы) , тогда кнопка поиска будет отключена .
  • Когда есть текст без пробелов , тогда кнопка поиска включена .
  • Когда пользователь нажимает кнопку поиска текстовое поле и кнопка поиска оба отключены .
  • Когда возвращаются результаты текстовое поле и кнопка поиска оба включены .

У меня есть эти наблюдаемые (созданные с помощью некоторых методов расширения для стандартных событий) для работы с:

IObservable<IEvent<TextChangedEventArgs>> textBox.TextChangedObservable()
IObservable<IEvent<RoutedEventArgs>> button.ClickObservable()
IObservable<IEvent<LoadingDataEventArgs>> dataSource.LoadingDataObservable()
IObservable<IEvent<LoadedDataEventArgs>> dataSource.LoadedDataObservable()

У меня есть эти запросы, которые работают в данный момент:

IObservable<bool> dataSourceIsBusy =
    dataSource.LoadingDataObservable().Select(x => true)
    .Merge(dataSource.LoadedDataObservable().Select(x => false));

IObservable<string> textBoxText =
    from x in textBox.TextChangedObservable()
    select textBox.Text.Trim();

IObservable<bool> textBoxTextIsValid =
    from text in textBoxText
    let isValid = !String.IsNullOrEmpty(text)
    select isValid;

IObservable<string> searchTextReady =
    from x in button.ClickObservable()
    select textBox.Text.Trim();

И затем эти подписки, чтобы связать все это:

buttonIsEnabled.Subscribe(x => button.IsEnabled = x);
dataSourceIsBusy.Subscribe(x => textBox.IsEnabled = !x);
searchTextReady.Subscribe(x => this.ExecuteSearch(x));

(я сохраняю ссылки на одноразовые товары, возвращаемые Subscribe методов.Я добавил .ObserveOnDispatcher() к каждой наблюдаемой, чтобы обеспечить выполнение кода в потоке пользовательского интерфейса.)

Теперь, пока это работает, есть одна вещь, которая меня беспокоит.Оператор select в searchTextReady вызывает textBox.Text.Trim(), чтобы получить текущий обрезанный текст поиска, но я уже сделал это в наблюдаемой textBoxText.Я действительно не хочу повторяться, поэтому я хотел бы создать запрос, который объединяет эти наблюдаемые, и вот где я терплю неудачу.

Когда я пытаюсь выполнить следующий запрос, я получаю повторные входящие вызовы для выполненияпоиск:

IObservable<string> searchTextReady =
    from text in textBoxText
    from x in button.ClickObservable()
    select text;

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

IObservable<string> searchTextReady =
    from text in button.ClickObservable()
        .CombineLatest(textBoxText, (c, t) => t)
    select text;

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

IObservable<string> searchTextReady =
    from text in textBoxText
      .SkipUntil(button.ClickObservable())
      .TakeUntil(dataSource.LoadingDataObservable())
    select text;

Есть идеи, как мне это сделать?

Ответы [ 4 ]

5 голосов
/ 20 октября 2010

Подобные вещи сами по себе являются хитрыми, поэтому я закончил тем, что написал библиотеку MV-VM + Rx , чтобы помочь мне - оказывается, что с этой библиотекой эта задача довольно проста;вот весь код, мой блог объясняет, как работают эти классы:

public class TextSearchViewModel
{
    public TextSearchViewModel
    {
        // If there is no text (or whitespace only) then the search button is disabled.
        var isSearchEnabled = this.ObservableForProperty(x => x.SearchText)
            .Select(x => !String.IsNullOrWhitespace(x.Value));

        // Create an ICommand that represents the Search button
        // Setting 1 at a time will make sure the Search button disables while search is running
        DoSearch = new ReactiveAsyncCommand(isSearchEnabled, 1/*at a time*/);

        // When the user clicks search the text box and the search button are both disabled.
        var textBoxEnabled = DoSearch.ItemsInflight
            .Select(x => x == 0);

        // Always update the "TextboxEnabled" property with the latest textBoxEnabled IObservable
        _TextboxEnabled = this.ObservableToProperty(textBoxEnabled, 
            x => x.TextboxEnabled, true);

        // Register our search function to run in a background thread - for each click of the Search
        // button, the searchResults IObservable will produce a new OnNext item
        IObservable<IEnumerable<MyResult>> searchResults = DoSearch.RegisterAsyncFunction(textboxText => {
            var client = new MySearchClient();
            return client.DoSearch((string)textboxText);
        });

        // Always update the SearchResults property with the latest item from the searchResults observable
        _SearchResults = this.ObservableToProperty(searchResults, x => x.SearchResults);
    }

    // Create a standard INotifyPropertyChanged property
    string _SearchText;
    public string SearchText {
        get { return _SearchText; }
        set { this.RaiseAndSetIfChanged(x => x.SearchText, value); }
    }

    // This is a property who will be updated with the results of an observable
    ObservableAsPropertyHelper<bool> _TextboxEnabled;
    public bool TextboxEnabled {
        get { return _TextboxEnabled.Value; }
    }

    // This is an ICommand built to do tasks in the background
    public ReactiveAsyncCommand DoSearch { get; protected set; }

    // This is a property who will be updated with the results of an observable
    ObservableAsPropertyHelper<IEnumerable<MyResult>> _SearchResults;
    public IEnumerable<MyResult> SearchResults {
        get { return _SearchResults.Value; }
    }
} 
1 голос
/ 25 января 2011

Спасибо всем за помощь в этом вопросе. Я не мог никому дать ответ, потому что обнаружил, что решение заключается в использовании оператора .Switch(), и никто не дал такого ответа.

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

Часть моей первоначальной проблемы в том, что я не хотел повторять код. Поэтому я ввел Func<string, bool> textIsValid, который можно использовать повторно в моем IObservable<bool> textBoxTextIsValid и в моем желаемом IObservable<string> searchTextReady.

Func<string, bool> textIsValid = t => !String.IsNullOrEmpty(t);

Теперь searchTextReady можно определить следующим образом:

IObservable<string> searchTextReady =
    (from text in textBoxText
     select (from x in button.ClickObservable().TakeUntil(textBoxText)
             where textIsValid(text)
             select text)
    ).Switch();

Итак, .Switch() работает, беря IObservable<IObservable<string>> и превращая его в IObservable<string>.

Легко создать IObservable<IObservable<string>> - просто напишите запрос к наблюдаемой и выберите другую наблюдаемую.

Например:

IObservable<Unit> xs= ...;
IObservable<string> ys= ...;

IObservable<IObservable<string>> zss = xs.Select(x => ys);

IObservable<string> zs = zss.Switch();

Но это не делает ничего особенно полезного.

Если оно изменено на:

IObservable<Unit> clicks = ...;
IObservable<string> texts = ...;
Func<string, bool> isValid = ...;

IObservable<IObservable<string>> zss =
    texts.Select(t =>
        clicks
            .Take(1)
            .TakeUntil(texts)
            .Where(x => isValid(t))
            .Select(x => t));

IObservable<string> zs = zss.Switch();

Окончательная наблюдаемая может быть описана как:

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

Надеюсь, этот ответ поможет кому-то еще.

0 голосов
/ 21 октября 2010

DistinctUntilChanged - это то, что вы ищете.

Например, что-то вроде этого должно работать:

// low level observables
var dataSourceLoading = ... // "loading" observable
var dataSourceLoaded = ... // "loaded" observable
var textChange = Observable.FromEvent<TextChangedEventArgs>(MyTextBox, "TextChanged");
var click = Observable.FromEvent<RoutedEventArgs>(MyButton, "Click");

// higher level observables
var text = textChange.Select(_ => MyTextBox.Text.Trim());
var emptyText = text.Select(String.IsNullOrWhiteSpace);
var searchInProgress = dataSourceLoading.Select(_ => true).Merge(dataSourceLoaded.Select(_ => false));

// enable/disable controls
searchInProgress.Merge(emptyText)
    .ObserveOnDispatcher()
    .Subscribe(v => MyButton.IsEnabled = !v);
searchInProgress
    .ObserveOnDispatcher()
    .Subscribe(v => MyTextBox.IsEnabled = !v);

// load data 
click
    .CombineLatest(text, (c,t) => new {c,t})
    .DistinctUntilChanged(ct => ct.c)
    .Subscribe(ct => LoadData(ct.t));
0 голосов
/ 19 октября 2010

Вы пробовали ForkJoin

Что-то вроде:

    IObservable<string> searchTextReady = Observable.ForkJoin(textBoxText, button.ClickObservable());
    searchTextReady.Subscribe( ....
...