Thursday, May 5, 2011

Beatifull Code Part 2 – Combining RIA Services with Reactive Extensions to build a push based search

A common scenario in most applications is looking up values based on the user input in a auto-complete fashion. If you want to do this with a RIA service you have to (let’s ignore MVVM for the sake of simplicity) subscribe to the TextChanged event, grab the Text value, get the query from the DomainContext, apply a Where clause using the given text, let’s say StartsWith, create a LoadOperation, wait for the completed event and on the completed event clear the contents of the previous search and write the new search result. Its really messy!

It is complex because you are converting a data source that is push based (the textbox pushes text to your application) in to a pull based logic and you have to handle all those async problems yourself. But imagine that your text box is pushing a projection of the data filtered by the typed value into the search result control. You can think of a “eih C# just put the xxxs that StartWith ‘text’ into the data grid!” How do we write this?

I use a very dummy user interface:

<sdk:DataGrid AutoGenerateColumns="True" Height="296" HorizontalAlignment="Left" Margin="26,178,0,0" Name="dataGrid1" VerticalAlignment="Top" Width="598" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="26,139,0,0" Name="textBox1" VerticalAlignment="Top" Width="278" />
<sdk:Label Height="28" HorizontalAlignment="Left" Margin="26,105,0,0" Name="label1" VerticalAlignment="Top" Width="120" Content="Name" />

And this is the code that pushes the TextChanged event into the service and back to the datagrid:

  var observableText = Observable.FromEvent<TextChangedEventArgs>(this.textBox1, "TextChanged");

  var text = (from change in observableText
             select ((TextBox)change.Sender).Text)
             .DistinctUntilChanged()
             .Throttle(TimeSpan.FromSeconds(1));

  var queryStream = from input in text
                    select context.GetCustomersQuery().Match(i => i.FirstName.StartsWith(input));

  var loadStream = from query in queryStream
                   select CreateLoad(query);

  var resultEventStream = from load in loadStream
                          select
                               Observable.FromEvent<EventArgs>(load, "Completed");

  var resultStream = from ev in resultEventStream
                     select ev
                         .ObserveOnDispatcher()
                         .Subscribe(current =>
                         {
                             var completedOperation = (LoadOperation<IndividualCustomer>)current.Sender;
                             this.customers.Clear();
                             foreach (var c in completedOperation.Entities)
                             {
                                 this.customers.Add(c);
                             }
                         });


  this.topmost = resultStream.Subscribe(); 

  this.dataGrid1.ItemsSource = this.customers;

The CreateLoad is just a helper function to allow customizing the query:

private LoadOperation<IndividualCustomer> CreateLoad(EntityQuery<IndividualCustomer> q)
{
    return this.context.Load(q);
}

The algorithm is quite simple, I take the event from the TextBox and convert it to an Observable of Text inputs. Then I take that Observable and create an Observable of Queries, them into an Observable of queries loading and  finally to an Observable of the results of loading those queries. I subscribe to this final one and conceptually I have the text that the user typed pushing loaded data back to me :).

Have fun,

Pedro

No comments: