ReactiveUI Goodies–Search

In my last post I talked about ReactiveList ReactiveUI Goodies – ReactiveList and I would like to dive a little bit more into collections. But before I do so, I would like to repeat a classical example of reactive programming –  Search. Search or Filtering is a basic feature in many Apps and as simple as it sounds it has its challenges. Especially if expensive API calls to remote servers are involved. The user experience is simple. The user wants to type a search text in a box and expects matching results. He might type at different speeds, correct typos or cancel the search altogether. Sound familiar? Good! Let’s do this the reactive way.

I have created a simple ViewModel that exposes three properties. SearchTerm of type string to hold the search term the user types, Results – a ReactiveList to display the search results and a Boolean called IsSearching to indicate whether a search is running. I also added a simple SearchService that looks for results in a 35k dictionary to make our demo App a bit more realistic.

public string SearchTerm
  {
      get { return _searchTerm; }
      set { this.RaiseAndSetIfChanged(ref _searchTerm, value); }
  }
 
  public ReactiveList<string> Results
  {
      get { return _results; }
      set { this.RaiseAndSetIfChanged(ref _results, value); }
  }
 
  public bool IsSearching
  {
      get { return _isSearching; }
      set { this.RaiseAndSetIfChanged(ref _isSearching, value); }
  }
Snippet 

Let’s start with the simplest version. As I described in ReactiveUI Goodies – Observing Properties it is very easy to listen to property changes of an object. In this case the SearchTerm property is what we are interested in. If we look at the code below the subscription clears the previous results and if an actual search term is given, then we ask the SearchService to look it up.

this.WhenAnyValue(x => x.SearchTerm)
   .Subscribe(async searchTerm =>
   {
       Results.Clear();
 
       if (!string.IsNullOrEmpty(searchTerm))
       {
           IsSearching = true;
 
           Debug.WriteLine($"Searching for: {searchTerm}");
           var results = await _searchService.Search(searchTerm);
 
           // We might have triggered multiple searches. But we only want the results that match the current search term.
           if (results?.Item1 == SearchTerm)
           {
               Results.AddRange(results.Item2);
               IsSearching = false;
           }
       }
 
       IsSearching = false;
   });
Snippet 2

While this code works just fine it is not very efficient assuming that Search is an expensive operation. Each key stroke will trigger our reactive subscription and therefore a search. As you can see in the code I added a Debug output to track the searches issued. This is what it looks like:

Searching for: h
Searching for: he
Searching for: hel
Searching for: hell
Searching for: hello

With just one line of code we can make this much better. The Throttle extension does exactly as the name suggests. It takes an IObservable and throttles its events for the given time. It acts like buffer. Whenever a new value comes in the within the time window then the old value get is replaced and the wait time starts over. That is exactly what we want in a scenario like this. As long as the user keeps typing we assume that he has not finished entering his search term. As soon as he pauses for a certain amount of time we take that as a go to search.

this.WhenAnyValue(x => x.SearchTerm)
    .Throttle(TimeSpan.FromSeconds(1.5), RxApp.MainThreadScheduler)
    .Subscribe(async searchTerm =>
    {
        Results.Clear();
        if (!string.IsNullOrEmpty(searchTerm))
        {
            IsSearching = true;
 
            Debug.WriteLine($"Searching for: {searchTerm}");
            var results = await _searchService.Search(searchTerm);
 
            if (results?.Item1 == SearchTerm)
            {
                Results.AddRange(results.Item2);
                IsSearching = false;
            }
        }
 
        IsSearching = false;
    });
Snippet 3

If we look now look at the Debug output, we can see that only one Search was issued.

Searching for: hello

Alright that looks much better. But something is a bit annoying. When deleting the search input we do not clear the old results immediately. We have to wait for the Throttle to time out. Let’s apply some more Reactive magic to fix that easily. The Do extension invokes an action on each element that appears in an Observable. You can think of it like an foreach loop into the future. If we invoke Do() before the Throttle() then we can execute our null or empty check immediately and clear old results. And since we already have handled the null or empty case, we can filter it out altogether before with the Where() clause.

this.WhenAnyValue(x => x.SearchTerm)
    .Do(x =>
    {
        if (string.IsNullOrEmpty(x))
        {
            Results.Clear();
            IsSearching = false;
        }
    })
    .Where(x => !string.IsNullOrEmpty(x))
    .Throttle(TimeSpan.FromSeconds(1.5), RxApp.MainThreadScheduler)
    .Subscribe(async searchTerm =>
    {
        Results.Clear();
        if (!string.IsNullOrEmpty(searchTerm))
        {
            IsSearching = true;
 
            Debug.WriteLine($"Searching for: {searchTerm}");
            var results = await _searchService.Search(searchTerm);
 
            if (results?.Item1 == SearchTerm)
            {
                Results.AddRange(results.Item2);
                IsSearching = false;
            }
        }
 
        IsSearching = false;
    });
Snippet 4

I hope this example opened your eyes to the power of the Reactive Extensions. Writing the same behavior with traditional code would be harder and way less compact. Of course this demo code can be improved further. Long running searches can span seconds and even with the Throttle in place we might have unnecessary/unwanted searches because the user corrects a typo or changes their mind. In those I would recommend to implement cancellation of the search task to further improve the efficiency.

Please find the example code at https://github.com/bitdisaster/practicalcode

Happy Coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s