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:
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:
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:
var list = new List<Rect>();
for (int i = 0; i < 10; i++)
list.Add(new Rect(i, i, i, i));
var view = new ListCollectionView(list);
view.Filter = (item) => { return ((Rect)item).X > 5; };
view.SortDescriptions.Add(new SortDescription("X", ListSortDirection.Descending));
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:
- 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
<t> class). - 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. - 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. - 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:
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:
public class ListCollectionView :
ICollectionViewEx,
IEditableCollectionView,
IComparer<object>
{
public ListCollectionView(object source)
{
_view = new List<object>();
_sort = new ObservableCollection<SortDescription>();
_sort.CollectionChanged += _sort_CollectionChanged;
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:
public object Source
{
get { return _source; }
set
{
if (_source != value)
{
_source = value;
if (_sourceNcc != null)
_sourceNcc.CollectionChanged -= _sourceCollectionChanged;
_sourceNcc = _source as INotifyCollectionChanged;
if (_sourceNcc != null)
_sourceNcc.CollectionChanged += _sourceCollectionChanged;
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:
void HandleSourceChanged()
{
var currentItem = CurrentItem;
_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);
}
}
}
}
OnVectorChanged(VectorChangedEventArgs.Reset);
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:
void HandleItemRemoved(int index, object item)
{
if (_filter != null && !_filter(item))
return;
if (index < 0 || index >= _view.Count || !object.Equals(_view[index], item))
index = _view.IndexOf(item);
if (index < 0)
return;
_view.RemoveAt(index);
if (index <= _index)
_index--;
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:
void HandleItemAdded(int index, object item)
{
if (_filter != null && !_filter(item))
return;
if (_sort.Count > 0)
{
_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;
}
_view.Insert(index, item);
if (index <= _index)
_index++;
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:
public Predicate<object> Filter
{
get { return _filter; }
set
{
if (_filter != value)
{
_filter = value;
HandleSourceChanged();
}
}
}
void _sort_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
HandleSourceChanged();
}
Next, we call the appropriate method when the source collection changes:
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:
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:
public IDisposable DeferRefresh()
{
return new DeferNotifications(this);
}
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())
{
}
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:
- 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. - 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 _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 _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:
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;
public ViewModel()
{
_songs = new ListCollectionView();
_songs.Source = Song.GetAllSongs();
_songs.Filter = FilterSong;
var sd = new SortDescription("Artist", ListSortDirection.Ascending);
_songs.SortDescriptions.Add(sd);
_sbUpdateFilter = new Storyboard();
_sbUpdateFilter.Duration = new Duration(TimeSpan.FromSeconds(1));
_sbUpdateFilter.Completed += (s,e) =>
{
_songs.Refresh();
};
_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:
public ICollectionView Songs
{
get { return _songs; }
}
public ICommand SortBy
{
get { return _cmdSort; }
}
public string FilterTerms
{
get { return _filterTerms; }
set
{
if (value != FilterTerms)
{
_filterTerms = value;
OnPropertyChanged("FilterTerms");
_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:
bool FilterSong(object song)
{
if (string.IsNullOrEmpty(FilterTerms))
return true;
foreach (var term in this.FilterTerms.Split(' '))
{
if (!FilterSongTerm((Song)song, term))
return false;
}
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;
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;
if (cv == null || string.IsNullOrEmpty(prop))
return false;
if (cv.SortDescriptions.Count > 0 &&
cv.SortDescriptions[0].PropertyName == prop)
return false;
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…>
<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:
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
.