Freigeben über


An Rx-Enabled WPF AutoComplete TextBox - Part 1/2

In this blog post we will focus on the Rx aspects of developing an autocomplete feature. In the next blog post we are going to work on a re-usable WPF auto-complete TextBox that uses Rx under the covers.

 Some months ago I delivered a Workshop on Reactive Extensions (Rx). If you never heard about Rx, you should read this first.

One little exercise I did with the workshop attendees was to develop a basic auto-complete TextBox that would query an online dictionary service and the results would be shown in the UI. The challenge was derived from the great Rx Workshop on Channel 9. Naturally, the challenge does not focus on developing a re-usable control, but on the Rx aspects.

What we ended with was a simple UI like that used Rx to handle all events:

 

 

A Simple Scenario but Some Interesting Challenges

Even if it looks simple to implement an UI that does the job, taking a look at the details reveals some (maybe) unexpected complexity:

  • How to do asynchronous service calls? The textbox input should be fed into a service endpoint. As it is the nature of remote endpoints, they won't answer instantly. That is, if we wait for the response on the UI thread our UI will freeze. Not acceptable. Thus we need to perform the endpoint calls asynchronously on a background thread.
  • How to we handle minimum input length? Most often it does not make sense to start querying for results when the input is below a certain threshold of characters. For a dictionary search 2 letters might make sense.
  • How do we avoid calling the service endpoint for every tiny keystroke? Some users type fast. Really fast. For those users we would like to avoid to create a request for every letter they put into the textbox. Only when a certain timespan of inactivity (300ms for example) is reached, we would like to send out a query to the remote service.
  • How do we avoid calling the service when there are no changes in contents? Imagine that a user pastes the same text twice into the textbox or an undo resets the text to the previous contents. In these cases we should avoid doing an extra roundtrip and simply ignore the text changed event.
  • How do we avoid mixing up the query result order? When we take the asynchronous approach we have to deal with a very difficult situation. A response to a request might appear out of order. That is another request's response reaches us earlier although the original request was sent later. There is no guarantee of order for us.

The good news is that Rx has for all these challenges some clever solutions.

Utilizing Rx on WPF Events

To transition from a classic event to an Rx Observable we can utilize the Obervable.FromEventPattern method. It allows us to create observable from the TextBox TextChanged Event:

 IObservable<EventPattern<TextChangedEventArgs>> textChangedObservable = 
     Observable.FromEventPattern<TextChangedEventArgs>(textBox, "TextChanged");

Looks a bit more complex than hooking a standard event handler, but it will pay off later. Think of Rx as an IEnumerable for events.

With the help of System.Reactive.Linq we can now start to work on this stream of TextChanged events. The first thing I would like to apply is a selection in order to get the thing we really want to deal with from our event stream.

 IObservable<string> textChangedObservable = 
    Observable.FromEventPattern<TextChangedEventArgs>(textBox, "TextChanged")
    .Select(evt => ((TextBox)evt.Sender).Text);

 What we now get is a nice stream of the user input that is currently present in the text box. If I would enter the search term "react" each with an individual key stroke the data that would be pushed to the subscriber our textChangedObservable would be the following:

  • r
  • re
  • rea
  • reac
  • react

Nice. We get the raw input now. Before we fine tune the input handling, let us think about how we can feed this data now into our query service.

Connecting to the Dictionary Service 

 

The dictionary service is brought in as a web service. The interface (excerpt):

  public DictionaryWord[] MatchInDict(string dictId, string word, string strategy);

 

and the async counterpart:

 public Task<DictionaryWord[]> MatchInDictAsync(string dictId, string word, string strategy);

As you can imagine the synchronous method is nothing we are interested in. Instead we will utilize one of Rx Observable Factory methods that convert a Task into an Observable. An Observable that is based on a Task will produce one result and than completes (or an error):

 var queryResultObservable = Observable.FromAsync(
             () => service.MatchInDictAsync("wn", input, "prefix"));
  

 We make the parameters for the dictId and strategy fixed and use the input as our variable. The goal is that the input is provided from the textbox' event data stream that we created before. How to combine these? With Linq!

 var results = from searchTerm in textChangedObservable
              from dictionaryServicesuggestions in ????
              select dictionaryServiceSuggestions;

What basically happens here is that we get updates from the text box text changes pushed into the searchTerm variable. What we want to do with this value is create a new observable for receiving the matching result to that search term. And here is our issue. We cannot just put the queryResultObservable here instead we need a little factory that will create us a new observable for every pushed down searchTerm. Let us introduce a little helper func to fill the gap:

 Func<string, IObservable<DictionaryWord[]>> getSuggestions = 
    (term) => Observable.FromAsync(() => service.MatchInDictAsync("wn", term, "prefix"));

 We insert that into our results observable:

var results = from searchTerm in textChangedObservable
              from dictionaryServicesuggestions in getSuggestions(searchTerm)
              select dictionaryServiceSuggestions;

Now we can start observing the results Observable for query results from the dictionary service. If we would like to put them in a WPF ListBox named listBox it could look like this:

 results.ObserveOn(listBox).Subscribe(
    words =>
        {
            listBox.Items.Clear();
            foreach (var word in words.Select(word => word.Word).Take(10).ToArray())
            {
                listBox.Items.Add(word);
            }
        });
  

The ObserveOn method is an extension method that comes with the Rx.WPF helpers. It will make sure that we receive the callbacks for new results on the Dispatcher thread and not on a background thread. We just take out 10 of the words in the result and add them to our listbox.

Fixing Issues 

From our list of challenges we can cross out the first one: we apply asynchronicity already. That is, the UI won't be blocked at any time by calling out to the remote dictionary service. However, there four other things are not addressed at the moment. Let us start to fix the minimum required input. We do not want to call the service before at least four characters are present in the textbox. To achieve that we simply apply a Where constraint to the textChangedObservable:

 var textChangedObservable = Observable
    .FromEventPattern<TextChangedEventArgs>(textBox, "TextChanged")
    .Select(evt => ((TextBox)evt.Sender).Text)
    .Where(text => text != null && text.Length >= 4);

This really shows the power of Rx. But it is getting even better. Let us make sure that we do not query the service if the actual value in the textbox has not changed. Here Rx' DistinctUntilChange comes handy:

 var textChangedObservable = Observable
    .FromEventPattern<TextChangedEventArgs>(textBox, "TextChanged")
    .Select(evt => ((TextBox)evt.Sender).Text)
    .Select(timestampedText => timestampedText.Value)
    .Where(text => text != null && text.Length >= 4)
    .DistinctUntilChanged();

 And finally let us make sure that the text changes do not trigger service calls too frequently. We apply Rx' Throttle:

 var textChangedObservable = Observable
    .FromEventPattern<TextChangedEventArgs>(textBox, "TextChanged")
    .Select(evt => ((TextBox)evt.Sender).Text)
    .Throttle(TimeSpan.FromMilliseconds(200))     
    .Select(timestampedText => timestampedText.Value)
    .Where(text => text != null && text.Length >= 4)
    .DistinctUntilChanged();

 

The textChangedObservable is looking great now. The constraints help us to do diligent on resources. One last thing though - and this is the nasty one. We need to make sure that the pending request events are discarded properly once a new search term in pushed down from the textChangedObservable. Lucky us, Rx can deal with that, too by applying TakeUntil. TakeUntil takes an IObservable as its parameter and will hold on to the decorated observable until the parameter observable publishes a new value. In this case we will simply hold on the dictionary result observable until a new input has been accepted: 

 var results = from searchTerm in textChangedObservable
                from dictionaryServiceSuggestions in getSuggestions(searchTerm)
                .TakeUntil(textChangedObservable) 
                select dictionaryServiceSuggestions;

Summary

 All well and good but not re-usable. What I would like to strive for is something like this - a reusable TextBox control:

 

 ... one that you can easily feed with the right connections from a ViewModel and use in various scenarios. But that's a topic for the second blog post in this series. The source code so far can be downloaded from SkyDrive. I added lots of debug output to the observables to ease understanding the process. The final version is availble on GitHub. See part 2 of the blog post for details.

Comments

  • Anonymous
    November 02, 2013
    Great article and very well explained