共用方式為


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

Comments

  • Anonymous
    October 30, 2009
    Since you mentioned it, why not make it generic?  It seems like it could infer in the typical case from the ctor's passed collection, and then you'd have a strong type instead of object, letting you avoid ToString :)
  • Anonymous
    October 30, 2009
    What happens in RaisePropChange()?
  • Anonymous
    October 30, 2009
    Could that be solved in WPF as well?
  • Anonymous
    October 30, 2009
    @Harry; I believe this class exists in WPF as well, so it should work fine. As to RaisePropChange: the class I copied these snippets from inhertits from my own implementation of INotifyPropertyChanged, and it exposes this method as a string-free way to raise property changed events.@NotGeneric: I didn't actually write this class; it's built into Silverlight.
  • Anonymous
    November 18, 2009
    "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."I'm a fan of MVVM and clean separation, etc, but I keep seeing this statement about the "ideal." My question is WHY is that ideal? I see people saying we want no code-behind, but what is the reasoning?I only ask because sometimes it seems people go to great lengths to do something just to avoid code behind. What is so wrong with that little piece of code behind? Not disagreeing, but trying to understand the compelling argument (i.e. if my user control is UI and the code behind manipulates UI, why is that bad and why would I want to take some UI and offload it to my data model?)
  • Anonymous
    November 18, 2009
    @Jeremy: I think it starts off with the assertion that testing stuff in the codebehind is difficult.From there you define a pattern (MVVM) that reduces/eliminates that codebehind.And then you get to a point where breaking that pattern - even if it doesn't really hurt testing - leads to a little cognitive dissonance, or a feeling of "no longer clean".
  • Anonymous
    April 19, 2010
    Thank you for your workCan this filtering method you are using work with a ListBox?? Can you please include a sample code to download so we can play with it.Thank you very muchray
  • Anonymous
    April 19, 2010
    @ray: Yeah, this should work fine with any UI element since it just exposes a collection of items like anything else.I don't have a simple project handy that I can package up right now... See if my code examples suffice; if not then feel free to email me questions and I'll do my best.
  • Anonymous
    October 25, 2010
    Maybe there's something I'm missing, but my list only updates when the textbox loses focus. Is there a way around this?
  • Anonymous
    October 25, 2010
    Mike, this is expected with Silverlight. Here's how you can fix it:dotneteers.net/.../make-a-silverlight-textbox-update-its-binding-on-every-character.aspx
  • Anonymous
    October 26, 2010
    Thanks Avi! That was so helpful! :) Much better than the ghetto way I was doing things...forcing focus on to another control than back to the textbox on the keyup event.  ;)
  • Anonymous
    December 20, 2010
    Has anyone found a way to use Regex with the filtering yet? My code doesn't seem to work just yet.System.Text.RegularExpressions.Regex searchTerm = new System.Text.RegularExpressions.Regex(filterText); //filterText is the user entered regex filter.m_view.Filter = (cc => searchTerm.IsMatch(((myList)cc).myDescription.ToUpper()) ||                              searchTerm.IsMatch(((myList)cc).myCode.ToUpper())                              ); //m_View is my PagedCollectionView
  • Anonymous
    December 20, 2010
    Scott, the fact you're using RegEx shouldn't matter at all. What I recommend is that you set some breakpoints within that long boolean and see what's going on. Most likely, it's a regex problem and not a filtering problem.One thing that might help is to break it up into lots of little steps so that you can more easily step through things and check variable values.