Click here to Skip to main content
Click here to Skip to main content

A WinRT CollectionView class with Filtering and Sorting

, 30 Sep 2014 CPOL
Rate this:
Please Sign up or sign in to vote.
The article presents an ICollectionView class that supports filtering and sorting.

Introduction

Data filtering and sorting are important features of .NET since it was introduced over 10 years ago. The DataTable class has supported both since .NET 1.0.

When WPF came along, it introduced the ICollectionView interface, which in addition to sorting and filtering supports grouping and currency (the notion of a current item).

Surprisingly, the ICollectionView interface specified in the WinRT version of the system libraries does not support sorting, filtering, or grouping. In WinRT, you can show a list of items on a grid, but there is no standard method for sorting or filtering this data.

This article describes ICollectionViewEx, an extended version of the ICollectionView interface and the implementation of a ListCollectionView class that implements it. With this class, you can add sorting and filtering to your data the same way you do it in your WPF, Silverlight, and Windows Phone applications.

The ListCollectionView class also implements the IEditableCollectionView interface, which allows advanced controls such as data grids to implement advanced editing features like canceling edits and adding new items.

The article includes a sample that demonstrates how you can use the ListCollectionView class to implement search box similar to the one found in applications such as iTunes. The search box applies a filter to the data source and selects items that contain all the terms typed in by the user in any of their properties. The filtered data can be used as a regular data source for any controls, even if they know nothing about the ICollectionViewEx interface.

Even though the sample is a Windows Store application, it uses the MVVM model and the ICollectionViewEx interface, which would make it trivial to create versions for Silverlight, WPF, or Windows Phone.

Note that ListCollectionView class does not implement grouping. That is a more advanced feature that will be left as an exercise for the reader.

The ICollectionViewEx Interface

The IColletionViewEx interface inherits from the standard ICollectionView interface and adds the members that are missing in the WinRT edition:

/// <summary>
/// Extends the WinRT ICollectionView to provide sorting and filtering.
/// </summary>
public interface ICollectionViewEx : ICollectionView
{
  bool CanFilter { get; }
  Predicate<object> Filter { get; set; }

  bool CanSort { get; }
  IList<SortDescription> SortDescriptions { get; }

  bool CanGroup { get; }
  IList<object> GroupDescriptions { get; }

  IEnumerable SourceCollection { get; }

  IDisposable DeferRefresh();
  void Refresh();
}

In addition to the members related to filtering and sorting, which we will implement later, the interface also has members related to grouping, for exposing the view’s SourceCollection, and for refreshing the view or deferring refreshes while the view is being modified.

All these elements are present in the WPF version of ICollectionView, and there are many libraries that rely on these being present.

The interface definition uses a SortDescription class and a ListSortDirection enum that also have to be defined:

public class SortDescription
{
  public SortDescription(string propertyName, ListSortDirection direction)
  {
    PropertyName = propertyName;
    Direction = direction;
  }
  public string PropertyName { get; set; }
  public ListSortDirection Direction { get; set; }
}

public enum ListSortDirection
{
  Ascending = 0,
  Descending = 1,
}

The IEditableCollectionView Interface

The IEditableCollectionView interface is also missing from WinRT. It exposes functionality used to provide advanced editing (allowing users to cancel edits) and adding items to the collection:

/// <summary>
/// Implements a WinRT version of the IEditableCollectionView interface.
/// </summary>
public interface IEditableCollectionView 
{
  bool CanAddNew { get; }
  bool CanRemove { get; }
  bool IsAddingNew { get; }
  object CurrentAddItem { get; }
  object AddNew();
  void CancelNew();
  void CommitNew();

  bool CanCancelEdit { get; }
  bool IsEditingItem { get; }
  object CurrentEditItem { get; }
  void EditItem(object item);
  void CancelEdit();
  void CommitEdit();
}

The first part of the interface deals with adding items to the collection. It is used by controls such as grids, which often expose a template for new rows where users can create elements simply by filling the template.

The second part deals with editing items. This is important because you don’t want to apply any sorting or filtering to a collection while an item is being edited. Doing so could cause the item to change position in the collection or even to be filtered out of view before you are done editing it. Also, the interface defines a CancelEdit method that restores the original state of the object, undoing all edits.

The ListCollectionView Class

The ListCollectionView class implements the ICollectionViewEx and IEditableCollectionView interfaces. It can be used like a regular WPF ListCollectionView. For example:

// create a list
var list = new List<Rect>();
for (int i = 0; i < 10; i++)
    list.Add(new Rect(i, i, i, i));

// create a view that filters and sorts the list
var view = new ListCollectionView(list);
view.Filter = (item) => { return ((Rect)item).X > 5; };
view.SortDescriptions.Add(new SortDescription("X", ListSortDirection.Descending));

// show the result
foreach (var r in view)
    Console.WriteLine(r);

Running this code produces the output you would expect:

 9,9,9,9
 8,8,8,8
 7,7,7,7
 6,6,6,6

The ListCollectionView class works as follows:

  1. It has a source collection that contains a list of elements. The source collection is exposed by the SourceCollection property. (If you want the ability to change the collection by adding and removing items, the source collection should implement the INotifyCollectionChanged interface, for example the ObservableCollection class).
  2. It has a filter predicate that selects which members of the source collection should be included in the view. The filter predicate is a function that takes an object as a parameter and returns true if the object should be included in the view, and false otherwise. By default, the filter is set to null, which causes all elements to be included in the view. The filter predicate is exposed by the Filter property.
  3. It has a collection of sort descriptors that specify which properties should be used to sort the elements included in the view and the sort direction. The sort descriptors are exposed by the SortDescriptors property.
  4. Finally, the view is a filtered and sorted list of elements. It is updated automatically when any of the three elements listed above change. The main challenge involved in implementing the ListCollectionView class is performing the updates efficiently.

The diagram below shows how these elements interact:

ICollectionView diagram

The ListCollectionView class listens to changes in the SourceCollection and SortDescriptors collections, and also to changes in the value of the Filter property.

When changes are detected in the SortDescriptors or Filter, the View collection is fully re-generated and the class raises a Reset notification to all listeners.

When changes are detected in the SourceCollection, the class tries to perform a minimal update.

For example, if a single item is added to the source, it is tested against the current filter. If the filter rejects the item, no further action is required. If the filter accepts the item (or if there is no filter), the item is inserted at the proper place in the view, taking the current sort into account. In this case, the class raises an ItemAdded notification.

Similarly, if a single item is deleted from the source and is present in the view, the item is simply removed from the view and an ItemRemoved notification is raised.

The minimal update feature improves application performance because it minimizes the number of full refresh notifications raised by the class. Imagine for example a data grid showing thousands of items. An item added event can be handled by creating a new row and inserting it at the proper position in the control. A full refresh, by contrast, would require the control to dispose of all its current rows and then create new ones.

Now that we know how the ListCollectionView is supposed to work, let’s look at the implementation. The ListCollectionView constructors are implemented as follows:

/// <summary>
/// Simple implementation of the <see cref="ICollectionViewEx"/> interface, 
/// which extends the standard WinRT definition of the <see cref="ICollectionView"/> 
/// interface to add sorting and filtering.
/// </summary>
public class ListCollectionView :
  ICollectionViewEx,
  IEditableCollectionView,
  IComparer<object>
  {
    public ListCollectionView(object source)
    {
      // create view (list exposed to consumers)
      _view = new List<object>();

      // create sort descriptor collection
      _sort = new ObservableCollection<SortDescription>();
      _sort.CollectionChanged += _sort_CollectionChanged;

      // hook up to data source
      Source = source;
    }
    public ListCollectionView() : this(null) { }

The constructor creates a _view list that will contain the filtered and sorted output list. It also creates a _sort collection that contains a list of sort descriptors to be applied to the view. The _sort collection is observable, so whenever it changes the view can be refreshed to show the new sort order.

Finally, the constructor sets the Source property to the source collection. Here is how the Source property is implemented:

/// <summary>
/// Gets or sets the collection from which to create the view.
/// </summary>
public object Source
{
 get { return _source; }
 set
 {
   if (_source != value)
   {
     // save new source
     _source = value;

     // listen to changes in the source
     if (_sourceNcc != null)
         _sourceNcc.CollectionChanged -= _sourceCollectionChanged;
     _sourceNcc = _source as INotifyCollectionChanged;
     if (_sourceNcc != null)
         _sourceNcc.CollectionChanged += _sourceCollectionChanged;

     // refresh the view
     HandleSourceChanged();
   }
 }
}

The setter stores a reference to the new source, connects a handler to its CollectionChanged event if that is available, and calls the HandleSourceChanged method to populate the view. The HandleSourceChanged method is where things start to get interesting:

// update view after changes other than add/remove an item 
void HandleSourceChanged()
{
  // keep selection if possible
  var currentItem = CurrentItem;

  // re-create view
  _view.Clear();
  var ie = Source as IEnumerable;
  if (ie != null)
  {
    foreach (var item in ie)
    {
      if (_filter == null || _filter(item))
      {
        if (_sort.Count > 0)
        {
          var index = _view.BinarySearch(item, this);
          if (index < 0) index = ~index;
            _view.Insert(index, item);
        }
        else
        {
          _view.Add(item);
        }
      }
    }
  }

  // notify listeners
  OnVectorChanged(VectorChangedEventArgs.Reset);

  // restore selection if possible
  CurrentItem = currentItem;
}

The HandleSourceChanged method performs a full refresh on the view. It starts by removing any existing items from the view. Then it enumerates the items in the source, applies the filter, and adds them to the view.

If the _sort list contains any members, then the view is sorted, and the position where the new item should be inserted is determined by calling the BinarySearch method provided by the List class.

Finally, the method calls the OnVectorChanged member to raise the VectorChanged event that is responsible for notifying any clients bound to the view.

If we were not concerned about efficiency, we could stop here. Calling the HandleSourceChanged method after any changes in the source collection or the filter/sort parameters would work. The only problem is it would work slowly. Any controls bound to the view would have to do a full refresh whenever a single item was added or removed from the source for example.

The ListCollectionView class has methods that deal with item addition and removal very efficiently. These methods are implemented as follows:

// remove item from view
void HandleItemRemoved(int index, object item)
{
  // no update needed if the item was filtered out of the view
  if (_filter != null && !_filter(item))
    return;

  // compute index into view
  if (index < 0 || index >= _view.Count || !object.Equals(_view[index], item))
    index = _view.IndexOf(item);
  if (index < 0)
    return;

  // remove item from view
  _view.RemoveAt(index);

  // if item was below our cursor, update cursor 
  if (index <= _index)
    _index--;

  // notify listeners
  var e = new VectorChangedEventArgs(CollectionChange.ItemRemoved, index, item);
  OnVectorChanged(e);
}

The HandleItemRemoved method starts by checking whether the item that was removed from the source has not been filtered out of the view. If that is the case, then the view hasn’t changed and nothing needs to be updated.

Next, the method determines the index of the item removed in the current view. If the view is filtered or sorted, the index passed to the method is invalid, and the actual index is determined by calling the IndexOf method. Once the item index is known, the item is removed from the view.

If the item removed was above the view’s current item (determined by the _index variable), then the view’s index is adjusted so the current item remains current. In this case, the view’s CurrentPosition property will change, but the CurrentItem property will remain the same.

Finally, the method calls the OnVectorChanged event to notify listeners that the view has changed.

The HandleItemAdded method is responsible for updating the view when items are added to the source collection:

// add item to view
void HandleItemAdded(int index, object item)
{
  // if the new item is filtered out of view, no work
  if (_filter != null && !_filter(item))
    return;

  // compute insert index
  if (_sort.Count > 0)
  {
    // sorted: insert at sort position
    _sortProps.Clear();
    index = _view.BinarySearch(item, this);
    if (index < 0) index = ~index;
  }
  else if (_filter != null)
  {
    var visibleBelowIndex = 0;
    for (int i = index; i < _sourceList.Count; i++)
    {
      if (!_filter(_sourceList[i]))
        visibleBelowIndex++;
    }
    index = _view.Count - visibleBelowIndex;
  }

  // add item to view
  _view.Insert(index, item);

  // keep selection on the same item
  if (index <= _index)
    _index++;

  // notify listeners
  var e = new VectorChangedEventArgs(CollectionChange.ItemInserted, index, item);
  OnVectorChanged(e);
}

As before, the method starts by checking whether the item that was added to the original collection should be included in the view. If not, then there’s no work to be done.

Next, the method determines the index where the new item should have in the view. If the view is sorted, the index is determined by calling the BinarySearch method as before. This will work whether or not the view is filtered.

If the view is not sorted, but is filtered, then the index of the item in the view is determined by counting how many items in the source collection are below the new item and are not filtered out of view. The index of the item in the view is then obtained by subtracting this number from the number of items in the view. This ensures that items will appear in the view in the same order they appear in the source list.

Once the item index is known, the item is added to the view.

As before, the view’s index is updated if the new item was inserted above the view’s current item.

Finally, the method calls the OnVectorChanged event to notify listeners that the view has changed.

Now that we have the three update methods in place, the next step is to call them from the right places: first, we call HandleSourceChanged when the sort descriptor collection or the filter predicate change. In both cases, the view has to be completely refreshed:

/// <summary>
/// Gets or sets a callback used to determine if an item is suitable for
/// inclusion in the view.
/// <summary>
public Predicate<object> Filter
{
  get { return _filter; }
  set
  {
    if (_filter != value)
    {
       _filter = value;
       HandleSourceChanged();
    }
  }
}
// sort changed, refresh view
void _sort_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
  HandleSourceChanged();
}

Next, we call the appropriate method when the source collection changes:

// the source has changed, update view
void _sourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
  switch (e.Action)
  {
    case NotifyCollectionChangedAction.Add:
      if (e.NewItems.Count == 1)
        HandleItemAdded(e.NewStartingIndex, e.NewItems[0]);
      else
        HandleSourceChanged();
      break;

    case NotifyCollectionChangedAction.Remove:
      if (e.OldItems.Count == 1)
        HandleItemRemoved(e.OldStartingIndex, e.OldItems[0]);
      else
        HandleSourceChanged();
      break;

    case NotifyCollectionChangedAction.Move:
    case NotifyCollectionChangedAction.Replace:
    case NotifyCollectionChangedAction.Reset:
      HandleSourceChanged();
      break;

    default:
      throw new Exception(
        "Unrecognized collection change notification" +
        e.Action.ToString());
  }
}

Finally, we call the HandleSourceChanged method in response to the Refresh method, which is public:

/// <summary>
/// Update the view from the current source, using the current filter 
/// and sort settings.
/// </summary>
public void Refresh()
{
  HandleSourceChanged();
}

This concludes the implementation of the filtering and sorting logic.

Deferred Notifications

Deferred notifications allow callers to suspend change notifications while they make extensive changes to the view. For example, suspending notifications is usually a good idea when adding items in bulk or applying several filter definitions.

The deferred notification mechanism in the ICollectionViewEx interface is exposed by a single member, the DeferRefresh method. The method is implemented as follows:

/// <summary>
/// Enters a defer cycle that you can use to merge changes to the
/// view and delay automatic refresh.
/// </summary>
public IDisposable DeferRefresh()
{
  return new DeferNotifications(this);
}
/// <summary>
/// Class that handles deferring notifications while the view is modified.
/// </summary>
class DeferNotifications : IDisposable
{
  ListCollectionView _view;
  object _currentItem;
  internal DeferNotifications(ListCollectionView view)
  {
    _view = view;
    _currentItem = _view.CurrentItem;
    _view._updating++;
  }
  public void Dispose()
  {
    _view.MoveCurrentTo(_currentItem);
    _view._updating--;
    _view.Refresh();
  }
}

The DeferRefresh method returns an internal DeferNotifications object that implements the IDisposable interface. The usage pattern is as follows:

using (view.DeferRefresh())
{
  // make extensive modifications to the view
}

The call to DeferRefresh creates a DeferNotifications object that increments the _updating counter in the ListCollectionView. While the _updating counter is greater than zero, the view will not raise any notifications.

At the end of the block, the DeferNotifications object goes out of scope, which automatically invokes its Dispose method. The Dispose method decrements the _updating counter and calls the Refresh method to restore the updates.

This pattern is better than the alternative BeginUpdate/EndUpdate methods because it makes very easy to scope the part of the code where notifications are suspended. It also makes sure notifications are properly restored even if there are exceptions within the code block (you don't have to write an explicit 'finally' clause).

Other ICollectionView Methods

The sections above discussed the implementation of the sorting and filtering methods which are present in the ICollectionViewEx but are not in the WinRT version of the ICollectionView interface.

Because the ICollectionViewEx interface inherits from ICollectionView, our ListCollectionView class must also implement those methods.

Fortunately, those methods are relatively simple. They fall into two broad categories:

  1. List operations: The ListCollectionView class delegates list operations to its _sourceList field, which is simply the source collection cast to an IList object that provides all the methods needed (such as Add, Remove, Contains, IndexOf, etc). If the source collection is not an IList, then the IsReadOnly property will return true and none of these methods will be available.
  2. Cursor operations: The ListCollectionView class keeps track of which item is currently selected, and exposes this information through members such as the CurrentItem and CurrentPosition properties, several MoveCurrentTo methods, and CurrentChanging/ CurrentChanged events. All of these properties, methods, and events are controlled by the _index property that was mentioned earlier.

Because these methods are so simple, we will not list them here. Please refer to the source code if you are interested in the implementation details.

IEditableCollectionView Implementation

The IEditableCollectionView implementation is relatively simple. The first part of the interface is related to editing items. The code is as follows:

// object being edited:
object _editItem;

public bool CanCancelEdit { get { return true; } }
public object CurrentEditItem { get { return _editItem; } }
public bool IsEditingItem { get { return _editItem != null; } }
public void EditItem(object item)
{
  var ieo = item as IEditableObject;
  if (ieo != null && ieo != _editItem)
    ieo.BeginEdit();
  _editItem = item;
}
public void CancelEdit()
{
  var ieo = _editItem as IEditableObject;
  if (ieo != null)
    ieo.CancelEdit();
  _editItem = null;
}
public void CommitEdit()
{
  if (_editItem != null)
  {
    var item = _editItem;
    var ieo = item as IEditableObject;
    if (ieo != null)
      ieo.EndEdit();
   _editItem = null;
    HandleItemChanged(item);
  }
}

The implementation consists of keeping track of the object being edited and calling its IEditableObject methods at the proper times. This allows a user to type the escape key while editing an object in a data grid, for example, to cancel all the edits and restore the object’s original state.

The CommitEdit method calls the HandleItemChanged method to ensure that the new item is properly filtered and sorted in the view.

The second part of the IEditableCollectionView interface is related to adding items to the view, and is implemented as follows:

// object being added:
object _addItem;

public bool CanAddNew { get { return !IsReadOnly && _itemType != null; } }
public object AddNew()
{
  _addItem = null;
  if (_itemType != null)
  {
    _addItem = Activator.CreateInstance(_itemType);
    if (_addItem != null)
      this.Add(_addItem);
  }
  return _addItem;
}
public void CancelNew()
{
  if (_addItem != null)
  {
    this.Remove(_addItem);
    _addItem = null;
  }
}
public void CommitNew()
{
  if (_addItem != null)
  {
    var item = _addItem;
    _addItem = null;
    HandleItemChanged(item);
}
}
public bool CanRemove { get { return !IsReadOnly; } }
public object CurrentAddItem { get { return _addItem; } }
public bool IsAddingNew { get { return _addItem != null; } }

The AddNew method creates new elements of the appropriate type using the Activator.CreateInstance method. New items are appended to the view and are not sorted or filtered until the CommitNew method is called.

This logic allows controls such as data grids to provide a “new row” template. When the user starts typing into the template, an item is automatically added to the view. When the user moves the cursor to a new row on the grid, it calls the CommitNew method and the view is refreshed. If the user presses the escape key before committing the new row, the data grid calls the CancelNew method and the new item is removed from the view.

MyTunes Sample Application

To demonstrate how you can use the ListCollectionView class, we created a simple MVVM application called MyTunes. The application loads a list of songs from a resource file and displays the songs in a GridView control.

The user can search for songs by typing terms into a search box. For example, typing “hendrix love” will show only songs that contain the words “hendrix” and “love” in their title, album, or artist name. The user can also sort the songs by title, album, or artist by clicking one of the buttons above the list.

The image below shows what the application looks like:

MyTunes application screenshot

The MyTunes ViewModel

The ViewModel class exposes a collection of songs and methods to filter and sort the collection. Here is the declaration and constructor:

public class ViewModel : INotifyPropertyChanged
{
  ListCollectionView _songs;
  string _filterTerms;
  Storyboard _sbUpdateFilter;
  ICommand _cmdSort;

  // ** ctor
  public ViewModel()
  {
    // expose songs as an ICollectionViewEx
    _songs = new ListCollectionView();
    _songs.Source = Song.GetAllSongs();
    _songs.Filter = FilterSong;

    // sort by Artist by default
    var sd = new SortDescription("Artist", ListSortDirection.Ascending);
    _songs.SortDescriptions.Add(sd);

    // use a storyboard to update filter after a delay
    _sbUpdateFilter = new Storyboard();
    _sbUpdateFilter.Duration = new Duration(TimeSpan.FromSeconds(1));
    _sbUpdateFilter.Completed += (s,e) => 
    {
      // refresh collection view to apply updated filter
      _songs.Refresh();
    };

    // command to sort the view
    _cmdSort = new SortCommand(this);
  }

The constructor starts by declaring a ListCollectionView to hold the songs, setting its Source property to a raw list of songs loaded from a local resource, and setting the Filter property to a FilterSong method that is responsible for selecting the songs that will be included in the view. It also initializes the SortDescriptions property to sort the songs by artist by default.

Next, the constructor sets up a StoryBoard that will be used to refresh the view one second after the user stops changing the search terms. This is more useful than refreshing the list after each keystroke.

Finally, the constructor creates an ICommand object that will be responsible for sorting the view according to different properties.

The object model for the ViewModel class is implemented as follows:

// ** object model
public ICollectionView Songs
{
  get { return _songs; }
}
public ICommand SortBy
{
  get { return _cmdSort; }
}
public string FilterTerms
{
  get { return _filterTerms; }
  set
  {
    if (value != FilterTerms)
    {
      // change the property
      _filterTerms = value;
      OnPropertyChanged("FilterTerms");

      // start timer to update the filter after one second
      _sbUpdateFilter.Stop();
      _sbUpdateFilter.Seek(TimeSpan.Zero);
      _sbUpdateFilter.Begin();
    }
  }
}

The ViewModel has only three properties.

The Songs property exposed the filtered and sorted collection as a standard ICollectionView. This is the property that will be bound to the ItemsSource property of the control responsible for showing the songs. In our sample this will be a GridView control.

The SortBy property exposes an ICommand object that will be bound to the Command property on buttons used to sort the collection.

Finally, the FilterTerms property contains a string with terms that will be used to search the list. When the property value changes, the code starts a Storyboard that will refresh the view after a one second delay. This is done so users can type into a search box without having the view refresh after each keystroke.

The remaining parts of the implementation are as follows:

// ** implementation
bool FilterSong(object song)
{
  // no filter term, pass always
  if (string.IsNullOrEmpty(FilterTerms))
    return true;

  // test each term in the filter terms
  foreach (var term in this.FilterTerms.Split(' '))
  {
    if (!FilterSongTerm((Song)song, term))
      return false;
  }

  // passed all
  return true;
}
static bool FilterSongTerm(Song song, string term)
{
  return 
    string.IsNullOrEmpty(term) ||
    song.Name.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 ||
    song.Album.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1 ||
    song.Artist.IndexOf(term, StringComparison.OrdinalIgnoreCase) > -1;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propName)
{
  if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs(propName));
}}

The FilterSong method, assigned to the ListCollectionView’s Filter property in the constructor, is responsible for determining which songs should be included in the view. It does this by splitting the filter terms into an array of strings and returning true only for songs that contain all terms in their Name, Album, or Artist property.

The SortCommand class exposed by the ViewModel’s SortBy property is implemented as follows:

class SortCommand : ICommand
{
  ViewModel _vm;
  public SortCommand(ViewModel vm)
  {
    _vm = vm;

    // update CanExecute value when the collection is refreshed
    var cv = _vm.Songs as ListCollectionView;
    if (cv != null)
    {
      cv.VectorChanged += (s, e) =>
      {
        if (CanExecuteChanged != null)
          CanExecuteChanged(this, EventArgs.Empty);
      };
    }
  }
  public event EventHandler CanExecuteChanged;
  public bool CanExecute(object parameter)
  {
    var prop = parameter as string;
    var cv = _vm.Songs as ListCollectionView;

    // check that we have a property to sort on
    if (cv == null || string.IsNullOrEmpty(prop))
      return false;

    // check that we are not already sorted by this property
    if (cv.SortDescriptions.Count > 0 &&
      cv.SortDescriptions[0].PropertyName == prop)
      return false;

    // all seems OK
    return true;
  }
  public void Execute(object parameter)
  {
    var prop = parameter as string;
    var cv = _vm.Songs as ListCollectionView;
    if (cv != null && !string.IsNullOrEmpty(prop))
    {
      using (cv.DeferRefresh())
      {
        cv.SortDescriptions.Clear();
        var sd = new SortDescription(
          prop, 
          ListSortDirection.Ascending);
        cv.SortDescriptions.Add(sd);
      }
    }
  }
}

The class implements a CanExecute method that returns false when the collection is already sorted by the given parameter, and true otherwise. This causes buttons bound to the command to be automatically disabled when the view is already sorted by that parameter. For example, clicking the 'sort by Artist' button will sort the collection by artist and will also disable the button until the list is sorted by some other property.

The implementation of the Execute method consists of updating the ListCollectionView’s SortDescriptions property. Notice how this is done within a DeferRefresh block so the view will only be refreshed once.

The MyTunes View

The view is implemented in pure XAML. The interesting parts are listed below:

<Page
  x:Class="MyTunes.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:local="using:MyTunes"
  mc:Ignorable="d">

  <Page.Resources>
    <local:ViewModel x:Key="_vm" />
    <local:DurationConverter x:Key="_cvtDuration" />
  </Page.Resources>

  <Grid 
    Background="{StaticResource ApplicationPageBackgroundThemeBrush}" 
    DataContext="{StaticResource _vm}">

    <Grid.RowDefinitions&hellip;>
      <RowDefinition Height="Auto" />
      <RowDefinition />
    </Grid.RowDefinitions>

This code creates an instance of the ViewModel class and assigns it to the DataContext property of the element that will serve as the layout root.

The content of the page is as follows:

<Grid Margin="20">
  <Grid.ColumnDefinitions>
    <ColumnDefinition />
    <ColumnDefinition Width="auto"/>
  </Grid.ColumnDefinitions>

  <TextBlock Text="MyTunes" FontSize="48" />

  <StackPanel Orientation="Horizontal"
    Grid.Column="1" Margin="12" VerticalAlignment="Center">

    <TextBlock Text="Sort" />
    <Button Content="By Song" 
       Command="{Binding SortBy}" CommandParameter="Name"/>
    <Button Content="By Album" 
       Command="{Binding SortBy}" CommandParameter="Album"/>
    <Button Content="By Artist" 
       Command="{Binding SortBy}" CommandParameter="Artist"/>

    <TextBlock Text="Search" />
    <local:ExtendedTextBox 
      Width="300" Margin="8 0" VerticalAlignment="Center" 
      Text="{Binding FilterTerms, Mode=TwoWay }" />
  </StackPanel>
</Grid>

The first element is a grid that contains the application title and the command bar.

The command bar contains three buttons bound to the ViewModel’s SortBy property and used to sort the view by song Name, Album, or Artist. The buttons are automatically disabled when the view is sorted by the property they represent, courtesy of the SortCommand class described earlier.

After the sort buttons, the command bar contains a text box bound to the ViewModel’s FilterTerms property.

Notice that we did not use a standard TextBox control. The reason for that is the WinRT TextBox only updates its binding source when it loses focus. In our app, the filter should be updated as the user types. To get the instant update behavior we want, we used the ExtendedTextBox control available on CodePlex:

https://mytoolkit.svn.codeplex.com/svn/WinRT/Controls/ExtendedTextBox.cs

The final piece of the view is the GridView element that will show the songs:

<GridView ItemsSource="{Binding Songs}" Grid.Row="1" >
  <GridView.ItemTemplate>
    <DataTemplate>
      <Border Margin="10" Padding="20" Background="#20c0c0c0" >
        <StackPanel Width="350">
          <TextBlock Text="{Binding Name}" FontSize="20" />
          <TextBlock Text="{Binding Album}" />
          <TextBlock>
            <Run Text="{Binding Artist}" />
              <Run Text=" (" />
              <Run Text="{Binding Duration,
                Converter={StaticResource _cvtDuration}}" />
              <Run Text=")" />
            </TextBlock>
          </StackPanel>
        </Border>
      </DataTemplate>
    </GridView.ItemTemplate>
  </GridView>
</Grid>

The GridView element is bound to the Songs property of the ViewModel. The ItemTemplate contains TextBlock elements bound to the properties of the Song class.

This is the complete application. The page has no code behind, as is typical in MVVM style applications. In fact, this application would be a completely standard MVVM app in Siverlight or in WPF. The only thing that makes it interesting is the fact that it is a WinRT (Windows Store) application and its ViewModel provides filtering and sorting, which are not supported natively by WinRT. That job is handled by our ListCollectionView class.

FilterControl Sample Application

In addition to the MyTunes sample, you may want to check out another interesting sample here:

http://our.componentone.com/samples/winrtxaml-filter

This sample shows how you can implement a touch-friendly FilterControl in WinRT. The FilterControl is bound to a collection view. As the user modifies the filter, the control updates the collection view's Filter property and any controls bound to the collection will automatically show the filtered results.

This is what the FilterControl in the sample looks like:

FilterControl for WinRT

To use the FilterControl, the user selects a property from the list on the left (for example "Country"). This causes the filter to show a histogram with the values of that property within the current view (for example sales for each country). The user then selects a specific value by sliding the histogram (for example sales in Germany).

The whole process makes it easy to filter your data using slide gestures, without typing, which makes this type of control great for use in tablet and phone applications.

The FilterControl allows you to attach ValueConverter objects to each property, so you can create histograms that show continents instead of specific countries or labels that describe value ranges (e.g. high, medium, and low sales).

Note that the FilterControl in the sample does not use our ICollectionViewEx interface, but a similar interface defined in a commercial package (the ComponentOne Studio for WinRT). If you want to run that sample using the ListCollectionView class presented here, you will have to make a few minor edits to the FilterControl class.

We will not get into the details for the FilterControl sample because it is beyond the scope of this article. But feeel free to download the source from the link above and use the control if you find it useful.

Conclusion

WinRT is an exciting new development platform. For me, one of its most interesting promises is the ability to re-use existing code developed for WPF and Silverlight applications. Unfortunately, there are some difficulties when doing this.

One problem is the fact that many names have changed (namespaces, class names, properties, events, methods and so on). These issues can usually be resolved by adding #if blocks to your code. This works, but your code will get a little messier and harder to maintain and debug.

A second and more serious problem is that some important functionality simply is not present in WinRT. A good example is the ability of the TextBox control to update its binding values when the text changes rather than when the control loses focus. In our sample application, we worked around that problem using the ExtendedTextBox available on CodePlex.

Another example of course is the re-definition of the ICollectionView interface, which motivated this article. In my opinion, the WinRT designers should have kept the original definition. They could have skipped the implementation of the filtering , sorting, and grouping methods in their own classes simply by having the CanFilter, CanSort, and CanGroup properties always return false. We would still have to implement that functionality, but at least we would not have to define a new version of the interface. This approach would have made it easier to port WPF and Silverlight controls to WinRT.

Perhaps future versions of WinRT will bring back the filtering, sorting, and grouping features of the ICollectionView interface. Until then, we will have to rely on custom implementations such as ListCollectionView.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Bernardo Castilho
Chief Technology Officer ComponentOne
United States United States
No Biography provided

Comments and Discussions

 
BugSubtle bug in handling a changed item. Pinmembermicahl16-Sep-14 19:54 
GeneralRe: Subtle bug in handling a changed item. PinmemberBernardo Castilho30-Sep-14 6:17 
QuestionTrying to add Groups using GroupKeyList class PinmemberBadshaRulez5-Sep-14 19:10 
QuestionSearch filter adds items twice, here's the fix [modified] Pinmemberbryan stump29-May-14 16:09 
AnswerRe: Search filter adds items twice, here's the fix PinmemberBernardo Castilho30-May-14 3:18 
QuestionView trying to keep selection in focus PinmemberMember 1076194020-Apr-14 4:10 
QuestionThanks and a question Pinmemberbgeerdes24-Feb-14 22:35 
AnswerRe: Thanks and a question [modified] PinmemberBernardo Castilho25-Feb-14 1:51 
Question5 Votes PinmemberMember 784777121-Feb-14 12:55 
AnswerRe: 5 Votes PinmemberBernardo Castilho22-Feb-14 8:30 
GeneralMy vote of 5 PinmemberStudi0sus17-Dec-13 7:55 
GeneralRe: My vote of 5 PinmemberBernardo Castilho17-Dec-13 8:03 
QuestionRefreshing ListCollectionView immediately PinmemberLDA STA27-Jun-13 15:11 
AnswerRe: Refreshing ListCollectionView immediately PinmemberBernardo Castilho27-Jun-13 20:31 
GeneralRe: Refreshing ListCollectionView immediately PinmemberLDA STA28-Jun-13 4:09 
GeneralRe: Refreshing ListCollectionView immediately PinmemberBernardo Castilho28-Jun-13 6:04 
QuestionDisplaying duplicates when sorting the items after performing some search [modified] Pinmemberkata.yaswanth28-May-13 21:04 
AnswerRe: Displaying duplicates when sorting the items after performing some search PinmemberBernardo Castilho29-May-13 5:28 
GeneralMy vote of 5 PinmemberFarhan Ghumra13-Mar-13 20:12 
GeneralRe: My vote of 5 PinmemberBernardo Castilho14-Mar-13 3:22 
GeneralRe: My vote of 5 PinmemberFarhan Ghumra14-Mar-13 3:25 
GeneralRe: My vote of 5 PinmemberBernardo Castilho14-Mar-13 3:28 
GeneralRe: My vote of 5 PinmemberFarhan Ghumra14-Mar-13 3:45 
GeneralRe: My vote of 5 PinmemberFarhan Ghumra14-Mar-13 4:09 
GeneralRe: My vote of 5 PinmemberBernardo Castilho19-Mar-13 4:06 
GeneralRe: My vote of 5 PinmemberFarhan Ghumra19-Mar-13 20:42 
GeneralAlternative approach: Filter & sort in the view-model PinmemberAdrian Alexander8-Mar-13 11:46 
QuestionViewModel [modified] PinmemberNavono00721-Feb-13 16:47 
AnswerRe: ViewModel PinmemberBernardo Castilho22-Feb-13 4:34 
GeneralRe: ViewModel PinmemberNavono00724-Feb-13 16:22 
GeneralRe: ViewModel PinmemberBernardo Castilho25-Feb-13 16:54 
QuestionNoob Question Pinmemberdmacuk16-Jan-13 3:23 
AnswerRe: Noob Question PinmemberBernardo Castilho16-Jan-13 3:36 
GeneralRe: Noob Question Pinmemberdmacuk16-Jan-13 3:50 
GeneralRe: Noob Question PinmemberBernardo Castilho16-Jan-13 5:59 
GeneralRe: Noob Question Pinmemberdmacuk16-Jan-13 6:12 
GeneralRe: Noob Question PinmemberBernardo Castilho16-Jan-13 6:33 
QuestionOutstanding Bernardo PinprotectorPete O'Hanlon15-Jan-13 5:33 
AnswerRe: Outstanding Bernardo PinmemberBernardo Castilho15-Jan-13 6:15 
GeneralRe: Outstanding Bernardo PinprotectorPete O'Hanlon15-Jan-13 6:40 
GeneralRe: Outstanding Bernardo PinmemberBernardo Castilho15-Jan-13 6:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 30 Sep 2014
Article Copyright 2013 by Bernardo Castilho
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid