ReactiveUI Goodies–IReactiveDerivedList Filtering 2

In my previous post ReactiveUI Goodies–IReactiveDerivedList Filtering 1 I demonstrated how we can utilize CreateDerivedCollection() to filter a collection based on a single root list. I would like to further explore that example and demonstrate a more sophisticated filtering solution. But, before we can dive into the actual filtering code, we need to do some preparations. First I would like to introduce a ViewModel for our TodoItem business class. This ViewModel will provide us with the INotifyPropertyChanged (INPC) implementation around our model class.

public class TodoItemViewModel : ReactiveObject
{
    private bool _isFilteredOut;
 
    public TodoItem Item { get; set; }
 
    public bool IsFilteredOut
    {
        get { return _isFilteredOut; }
        set { this.RaiseAndSetIfChanged(ref _isFilteredOut, value); }
    } 
 
    public TodoItemViewModel(TodoItem item, IObservable<Predicate<TodoItem>> filter)
    {
        Item = item;
 
        filter.Subscribe(x =>
        {
            IsFilteredOut = x(Item);
        });
       }
   }

We also could have easily changed the TodoItem class itself to implement INPC. However, in the real world, often classes from the Model layer can’t be changed and INPC is actually a child of the ViewModel layer anyhow. So to make this example a bit more realistic, a ViewModel it is. There is not much to our TodoItemViewModel. Of course, we need the TodoItem wrapping and in addition a property that indicates whether this particular instance should be filtered out. Since we want to change the filter via user input we need to know when the filter changes. Therefore, we provide an Observable of predicates over TodoItems.

On our MainViewModel class we also need to make some changes. First of all, our root list and derived list now carrying TodoItemViewModels instead of TodoItems. We also want to enable the user to not only filter by Done-state of an item but also by the title of an item via a text input.

private readonly DataService _dataService;
private ReactiveList<TodoItemViewModel> _rootList;
private IReactiveDerivedList<TodoItemViewModel> _items;
private int _count;
private string _filterText;
private bool _showTodoOnly;
 
public bool ShowTodoOnly
{
    get { return _showTodoOnly; }
    set { this.RaiseAndSetIfChanged(ref _showTodoOnly, value); }
}
 
public string FilterText
{
    get { return _filterText; }
    set { this.RaiseAndSetIfChanged(ref _filterText, value); }
}

Now that we are prepped, the filtering becomes very easy. First we enable a little option on our root list.

_rootList.ChangeTrackingEnabled = true;

The ChangeTrackingEnabled property tells the ReactiveList to not only track items being added or removed. But also changes that happen on the properties of the items in the list. In our case the IsFilteredOut property. Reporting item changes allows the derived list to react to it and re-evaluate the filter function we provided. We simply want to show only items where the IsFilteredOut property is false. While this is very powerful, I want not leave without a warning. As always, magic comes at a price. Tracking all properties in a large collection can become very expensive in terms of performance.

Items = _rootList.CreateDerivedCollection(x => x, x => !x.IsFilteredOut, (x, y) => x.Item.DueDate.CompareTo(y.Item.DueDate));

Now we have everything in place. But I mentioned before that we would like to filter based on user input. In order to tell our TodoViewModels that the filter criteria has changed, we need to provide an Observable of predicates. So let’s declare a property on MainViewModel to report the current filter criteria.

public Subject<Predicate<TodoItem>> FilterObservable { get; set; }

The generic Subject class implements IObservable and also allows us to push items into the Observable stream. With that setup we now can implement a UpdatetFilter() method that reflects the user input.

private void UpdateFilter()
{
    if (ShowTodoOnly)
    {
        if (string.IsNullOrEmpty(FilterText))
        {
            FilterObservable.OnNext(x => x.Done == false);
        }
        else
        {
            FilterObservable.OnNext(x => x.Done == false || !x.Title.Contains(FilterText));
        }
    }
    else
    {
        if (string.IsNullOrEmpty(FilterText))
        {
            FilterObservable.OnNext(x => false);
        }
        else
        {
            FilterObservable.OnNext(x => !x.Title.Contains(FilterText));
        }
    }
}

The only thing left is to call UpdateFilter() whenever the user input changes. The user has 2 choices, an option to only show open to-do items and/or filter based on text in the item title. We simply need to observe whenever either one of them changes. For the filter text we also want to apply what we learned from my Search example <link> and throttle the update calls as the user types.

this.ObservableForProperty(x => x.ShowTodoOnly)
    .Subscribe(_ =>
    {
        UpdateFilter();
    });
 
this.ObservableForProperty(x => x.FilterText)
     .Do(x =>
     {
         if (string.IsNullOrEmpty(x.Value))
         {
             UpdateFilter();
         }
     })
    .Where(x => !string.IsNullOrEmpty(x.Value))
    .Throttle(TimeSpan.FromSeconds(1))
    .Subscribe(_ =>
    {
        UpdateFilter();
    });

While I did add a bit more complexity than necessary for a simple demo, I hope it helps you to apply derived list in your real world scenarios. With the power of ReactiveList and CreateDerivedCollection()  you are able to create reactive and fluid filter experience for the user in a very elegant way.

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

Happy Coding!

One thought on “ReactiveUI Goodies–IReactiveDerivedList Filtering 2

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