Question

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 of the ones I found use Observable.FromEventPattern() together with the TextBox's TextChanged event:

I'm using WPF with MVVM so I can't exactly access the TextBox or its events directly.

I've also stumbled upon this answer which shows how Observable.FromEventPattern() can be used in an MVVM setting, but I was hoping for something better than capturing every single PropertyChanged event.

What is a good alternative to Observable.FromEventPattern() that can work nicely with WPF/MVVM?

Was it helpful?

Solution

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;
        }
    }
}

OTHER TIPS

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.

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top