Real-time list filtering with Silverlight, MVVM, and PagedCollectionView

The Model-View-ViewModel pattern is very good for forcing clean UI code. Ideally, you want to end up with zero code in your .xaml.cs file – everything should be data-bound.

As nice as this sounds, sometimes it can get so tempting to break this rule in order to do something that should be simple. Here’s one example:

You have a ListView and a TextBox in the view; they are bound to an ObservableCollection and a string in the ViewModel. You want them to behave such that entering text into that box causes the ListView to filter its items:

image

The initial train of thought is like this: In the event handler for the TextBox, address the ListView and hide all the items that don’t match the filter. At least one problem with this is that you’ve broken your promise to keep logic in the ViewModel.

The second train of thought is: Since the TextBox is bound to a property on the ViewModel, add code to the property setter to remove all the items from the ObservableCollection which don’t match the filter.

This will “work”, and suck. The problem is that you’ve actually removed data, not filtered it. When the user changes the filter text, your collection no longer has the items you’ve removed – you need to reload them.

This is where the PagedCollectionView comes in. It’s a wrapper around another collection which serves the following purposes:

  • You can bind UI elements to it, and it will work like any other collection.
  • When wrapping a collection that implements ICollectionChanged, it bubbles the notification changes.
  • It has properties for setting filters (as well as sorting, paging, grouping).

So what you do is insert this object between your UI and your ObservableCollection, and just modify the filters on it. The data remains untouched, but the UI reacts perfectly.

Before:

image

After:

image

 

Here’s a simple example of the steps to take:

 

1) Create both the ObservableCollection and the PagedCollectionView, but only expose the latter as a property:

 // This is the actual container of items that we're storing
private ObservableCollection<string> _RandomItemsBase;

// And this is the wrapper around it which we bind to. The PagedCollectionView
// allows for easy filtering, sorting, grouping.
private PagedCollectionView _RandomItems;
public PagedCollectionView RandomItems
{
    get { return _RandomItems; }
    set
    {
        _RandomItems = value;
        RaisePropChange(() => this.RandomItems);
    }
}

 

2) In the constructor, wrap the real collection with the PagedCollectionView:

 _RandomItemsBase = new ObservableCollection<string>();
RandomItems = new PagedCollectionView(_RandomItemsBase);

 

3) When the entered text changes, call a method to filter things:

 public string EnteredText
{
    get { return _EnteredText; }
    set {
        _EnteredText = value;
        RaisePropChange(() => this.EnteredText);
        UpdateFilter(); // Real-time filter
    }
}

 

4) And here’s the code to actually apply the filter to the view:

 private void UpdateFilter()
{
    if (string.IsNullOrEmpty(EnteredText))
        RandomItems.Filter = null;
    else
        RandomItems.Filter = (o) => o.ToString()
                  .Contains(EnteredText);

    // Make sure to force a refresh of the view
    RandomItems.Refresh();
}

There is one interesting thing about the above snippet. Notice the lambda expression used to filter takes one argument (“o”), and I just call ToString() on it. Since the PagedCollectionView isn’t templated, the filter delegate is called with an argument of type object. You can then cast that object to whatever type you know the underlying colleciton uses. In this case, I have an ObservableCollection<string> , so I just call the ToString() method.

 

That’s it. It gets even cooler when you start applying sorting, paging and grouping to this view, then bind to it with a DataGrid, or something similar.

 

Avi