I would like to implement a TextBox where, as you type, results appear instantly in another ListBox. I\'ve been looking for examples with Reactive Extensions (Rx), and all o
This is the kind of approach that I would use in this instance:
var query = Observable.FromEventPattern
<TextChangedEventHandler, TextChangedEventArgs>(
h => textBox1.TextChanged += h,
h => textBox1.TextChanged -= h)
.Throttle(TimeSpan.FromMilliseconds(100))
.ObserveOnDispatcher()
.Select(x => textBox1.Text)
.DistinctUntilChanged()
.Do(x => listBox1.Items.Clear())
.ObserveOn(Scheduler.Default)
.Select(x => executeSearch(x))
.Switch()
.ObserveOnDispatcher();
query.Subscribe(x => listBox1.Items.Add(x));
The executeSearch
code has the following signature: Func<string, IObservable<string>>
.
The important part with this query is the final Switch
statement. It turns an IObservable<IObservable<string>>
into a IObservable<string>
by only returning the results of the latest observable.
The calls to .ObserveOnDispatcher()
& .ObserveOn(Scheduler.Default)
ensure that different parts of the observable query happen on the correct threads.
Got this working with ReactiveUI.
The solution is based on a blog post at ReactiveUI, but the code there is a little bit out of date. I am hosting the solution on BitBucket for ease of access. It uses ReactiveUI 5.5.1.
This is the ViewModel from that solution. SearchText
is bound to a TextBox
in the View where the user types his query, while SearchResults
is bound to a ListBox
displaying the results.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Windows.Input;
using ReactiveUI;
namespace ReactiveExtensionsSearch
{
public class MainWindowViewModel : ReactiveObject
{
private string[] _repository = new string[]
{ "Mario", "Maria", "Gigi", "Jack", "James", "Jeremy" };
private ObservableAsPropertyHelper<ObservableCollection<string>> _searchResults;
private string _searchText;
private ICommand _executeSearchCommand;
public string SearchText
{
get
{
return _searchText;
}
set
{
this.RaiseAndSetIfChanged(ref _searchText, value);
}
}
public ObservableCollection<string> SearchResults
{
get
{
return _searchResults.Value;
}
}
public MainWindowViewModel()
{
var executeSearchCommand = new ReactiveCommand();
var results = executeSearchCommand.RegisterAsyncFunction(s => { return ExecuteSearch(s as string); });
_executeSearchCommand = executeSearchCommand;
this.ObservableForProperty<MainWindowViewModel, string>("SearchText")
.Throttle(TimeSpan.FromMilliseconds(800))
.Select(x => x.Value)
.DistinctUntilChanged()
.Where(x => !string.IsNullOrWhiteSpace(x))
.Subscribe(_executeSearchCommand.Execute);
_searchResults = new ObservableAsPropertyHelper<ObservableCollection<string>>(results, _ => raisePropertyChanged("SearchResults"));
}
private ObservableCollection<string> ExecuteSearch(string searchText)
{
var q = from s in _repository where s.ToLower().StartsWith(searchText.ToLower()) select s;
var results = new ObservableCollection<string>(q);
return results;
}
}
}
You can use a behavior to get this working. A great example is the UpdateBindingOnTextChanged behavior of Catel. It can be used like this:
<TextBox Text="{Binding SearchParam, Mode=TwoWay}">
<i:Interaction.Behaviors>
<catel:UpdateBindingOnTextChanged UpdateDelay="500" />
</i:Interaction.Behaviors>
</TextBox>
This will create a 500ms delay between the change and the actual update.