Reactive Extensions– Observable.FromAsyncPattern, dalsza część przykładu

W poprzednim poście pokazałem jak korzystać z funkcji FromAsyncPattern na przykładzie usługi sieciowej. Dzisiaj zaprezentuję kilka dodatkowych funkcji. Najpierw zdefiniujmy co chcemy uzyskać:

  1. Użytkownik może wpisać szukaną frazę w pole edycyjne.
  2. Usługa sieciowa ma za zadanie wyszukanie fraz wpisanych w pole zdefiniowane w punkcie 1.
  3. Wyłącznie frazy dłuższe niż 3 znaki mają być przetwarzane.
  4. Jeśli użytkownik wpisze dwa razy tą samą frazę to tylko pierwsza ma zostać wysłana do usługi (optymalizacja).
  5. Zdarzenie TextChanged jest wywoływane dla każdego wpisanego znaku. Z tego względu, poprzednie rozwiązanie wysyłało dużo niepotrzebnych zapytań. W celu optymalizacji wprowadzimy opóźnienie takie jakie jest np. w Intellisense – propozycje nie są wyświetlane za każdym razem gdy jest wpisywany pojedynczy znak ale dopiero gdy użytkownik chwilę odczeka.

Klasyczne rozwiązanie problemu, bez użycia RX mogłoby wyglądać następująco:

public partial class MainWindow : Window
{
   private string _previousText;
   private DateTime _lastRead;
   private const long MinimalTimeBetweenRequests = 1000*5;

   public MainWindow()
   {
       InitializeComponent();
   }
   private void TextBoxTextChanged(object sender, TextChangedEventArgs e)
   {
       TimeSpan timeDiff = DateTime.Now - _lastRead;
       if (termTextBox.Text.Length >= 3 && _previousText != termTextBox.Text && timeDiff.TotalMilliseconds > MinimalTimeBetweenRequests)
       {
           DictServiceSoapClient client = new DictServiceSoapClient("DictServiceSoap");
           _lastRead = DateTime.Now;
           client.BeginMatchInDict("wn", termTextBox.Text, "prefix", SearchCallback, client);
       }
       _previousText = termTextBox.Text;
   }
   private void SearchCallback(IAsyncResult result)
   {
       var client = (DictServiceSoapClient) result.AsyncState;
       DictionaryWord[] words = client.EndMatchInDict(result);            
       Dispatcher.Invoke(new Action(() => UpdateResults(words)));
   }
   private void UpdateResults(IEnumerable<DictionaryWord> words)
   {
       StringBuilder resultsBuilder = new StringBuilder();
       foreach (var word in words)
       {
           resultsBuilder.Append(word.Word);
           resultsBuilder.AppendLine();
       }
       results.Text = resultsBuilder.ToString();
   }
}

Szczególnie nie lubię callback’ów i definiowania pól w klasie, które naprawdę wykorzystywane są tylko w jednym miejscu.  Należy dążyć do sytuacji gdzie jest jak najmniej pól – upraszcza to czytanie kodu. Jeśli to tylko możliwe lepiej korzystać z zasobów lokalnych.

RX znacząco uprości powyższy problem. Najpierw warto jednak przyjrzeć się następującym metodom:

  1. Throttle – tłumi przetwarzanie danych. Dzięki temu łatwo zrealizować wymaganie numer 5. Wystarczy wywołać tą metodę z odpowiednim parametrem (czas) a RX zajmie się resztą.
  2. DistinctUntilChanged – zwraca wyłącznie jednorazowe, unikatowe wartości. Jeśli zdarzenie TextChanged generuje “Piotr”,”Paweł’,”Paweł na wyjściu pojawi się “Piotr”,”Paweł”. Z kolei jeśli zdarzenia dostarczają “Piotr”,”Paweł”,”Piotr” na wyjściu ukażę się identyczna sekwencja. Z tego względu DistinctUntilChanged nie jest tym samym co Distinct.

Zatem rozwiązanie za pomocą RX może wyglądać następująco:

public partial class MainWindow : Window
{
   public MainWindow()
   {
       InitializeComponent();

       DictServiceSoapClient client = new DictServiceSoapClient("DictServiceSoap");

       var dictionarySource =
           Observable.FromAsyncPattern<string, string, string, DictionaryWord[]>(client.BeginMatchInDict,
                                                                                 client.EndMatchInDict);
       var inputSource =
           Observable.FromEventPattern<TextChangedEventArgs>(termTextBox, "TextChanged").Select(i =>
                                                                                                ((TextBox) i.Sender)
                                                                                                    .Text);

       inputSource.Throttle(TimeSpan.FromSeconds(1)).
                   DistinctUntilChanged().
                   Where(input => input.Length >= 3).
                   SelectMany(input => dictionarySource("wn", input, "prefix")).
                   ObserveOn(SynchronizationContext.Current).
                   Subscribe(UpdateResults, () => MessageBox.Show("Completed"));
   }
   private void UpdateResults(IEnumerable<DictionaryWord> words)
   {
       StringBuilder resultsBuilder = new StringBuilder();
       foreach (var word in words)
       {
           resultsBuilder.Append(word.Word);
           resultsBuilder.AppendLine();
       }
       results.Text = resultsBuilder.ToString();
   }
}

Dzięki RX programista może opisywać swoją intencję za pomocą jednego zdania. Taki kod jest czytelniejszy ponieważ nie trzeba skakać z jednego miejsca do drugiego – wszystko jest zawarte w tym zdaniu.

Leave a Reply

Your email address will not be published.