In UWP apps, how can you group and sort an ObservableCollection and keep all the live notification goodness?
In most simple UWP examples I've seen, there is generally a ViewModel that exposes an ObservableCollection which is then bound to a ListView in the View. When items are added or removed from the ObservableCollection, the ListView automatically reflects the changes by reacting to the INotifyCollectionChanged notifications. This all works fine in the case of an unsorted or ungrouped ObservableCollection, but if the collection needs to be sorted or grouped, there seems to be no readily apparent way to preserve the update notifications. What's more, changing the sort or group order on the fly seems to throw up significant implementation issues.
++
Take a scenario where you have an existing datacache backend that exposes an ObservableCollection of very simple class Contact.
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string State { get; set; }
}
This ObservableCollection changes over time, and we want to present a realtime grouped and sorted list in the view that updates in response to changes in the datacache. We also want to give the user the option to switch the grouping between LastName and State on the fly.
++
In a WPF world, this is relatively trivial. We can create a simple ViewModel referencing the datacache that presents the cache's Contacts collection as-is.
public class WpfViewModel
{
public WpfViewModel()
{
_cache = GetCache();
}
Cache _cache;
public ObservableCollection<Contact> Contacts
{
get { return _cache.Contacts; }
}
}
Then we can bind this to a view where we implement a CollectionViewSource and Sort and Group definitions as XAML resources.
<Window .....
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">
<Window.DataContext>
<local:WpfViewModel />
</Window.DataContext>
<Window.Resources>
<CollectionViewSource x:Key="cvs" Source="{Binding Contacts}" />
<PropertyGroupDescription x:Key="stategroup" PropertyName="State" />
<PropertyGroupDescription x:Key="initialgroup" PropertyName="LastName[0]" />
<scm:SortDescription x:Key="statesort" PropertyName="State" Direction="Ascending" />
<scm:SortDescription x:Key="lastsort" PropertyName="LastName" Direction="Ascending" />
<scm:SortDescription x:Key="firstsort" PropertyName="FirstName" Direction="Ascending" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView ItemsSource="{Binding Source={StaticResource cvs}}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding LastName}" />
<TextBlock Text="{Binding FirstName}" Grid.Column="1" />
<TextBlock Text="{Binding State}" Grid.Column="2" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid Background="Gainsboro">
<TextBlock FontWeight="Bold"
FontSize="14"
Margin="10,2"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<StackPanel Orientation="Horizontal" Grid.Row="1">
<Button Content="Group By Initial" Click="InitialGroupClick" />
<Button Content="Group By State" Click="StateGroupClick" />
</StackPanel>
</Grid>
</Window>
Then when the user clicks on the GroupBy buttons at the bottom of the window we can we can group and sort on the fly in code-behind.
private void InitialGroupClick(object sender, RoutedEventArgs e)
{
var cvs = FindResource("cvs") as CollectionViewSource;
var initialGroup = (PropertyGroupDescription)FindResource("initialgroup");
var firstSort = (SortDescription)FindResource("firstsort");
var lastSort = (SortDescription)FindResource("lastsort");
using (cvs.DeferRefresh())
{
cvs.GroupDescriptions.Clear();
cvs.SortDescriptions.Clear();
cvs.GroupDescriptions.Add(initialGroup);
cvs.SortDescriptions.Add(lastSort);
cvs.SortDescriptions.Add(firstSort);
}
}
private void StateGroupClick(object sender, RoutedEventArgs e)
{
var cvs = FindResource("cvs") as CollectionViewSource;
var stateGroup = (PropertyGroupDescription)FindResource("stategroup");
var stateSort = (SortDescription)FindResource("statesort");
var lastSort = (SortDescription)FindResource("lastsort");
var firstSort = (SortDescription)FindResource("firstsort");
using (cvs.DeferRefresh())
{
cvs.GroupDescriptions.Clear();
cvs.SortDescriptions.Clear();
cvs.GroupDescriptions.Add(stateGroup);
cvs.SortDescriptions.Add(stateSort);
cvs.SortDescriptions.Add(lastSort);
cvs.SortDescriptions.Add(firstSort);
}
}
This all works fine, and the items are updated automatically as the data cache collection changes. The Listview grouping and selection remains unaffected by collection changes, and the new contact items are correctly grouped.The grouping can be swapped between State and LastName initial by user at runtime.
++
In the UWP world, the CollectionViewSource no longer has the GroupDescriptions and SortDescriptions collections, and sorting/grouping need to be carried out at the ViewModel level. The closest approach to a workable solution I've found is along the lines of Microsoft's sample package at
https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlListView
and this article
http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping
where the ViewModel groups the ObservableCollection using Linq and presents it to the view as an ObservableCollection of grouped items
public ObservableCollection<GroupInfoList> GroupedContacts
{
ObservableCollection<GroupInfoList> groups = new ObservableCollection<GroupInfoList>();
var query = from item in _cache.Contacts
group item by item.LastName[0] into g
orderby g.Key
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
GroupInfoList info = new GroupInfoList();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
where GroupInfoList is defined as
public class GroupInfoList : List<object>
{
public object Key { get; set; }
}
This does at least get us a grouped collection displayed in the View, but updates to the datacache collection are no longer reflected in real time. We could capture the datacache's CollectionChanged event and use it in the viewmodel to refresh the GroupedContacts collection, but this creates a new collection for every change in the datacache, causing the ListView to flicker and reset selection etc which is clearly suboptimal.
Also swapping the grouping on the fly would seem to require a completely seperate ObservableCollection of grouped items for each grouping scenario, and for the ListView's ItemSource binding to be swapped at runtime.
The rest of what I've seen of the UWP environment seems extremely useful, so I'm surprised to find something as vital as grouping and sorting lists throwing up obstacles...
Anyone know how to do this properly?
I've started putting together a library called GroupedObservableCollection that does something along these lines for one of my apps.
One of the key problems that I needed to solve was the refreshing of the original list that was used to create the group, i.e. I didn't want a user searching with slightly different criteria to cause the whole list to be refreshed, just the differences.
In its current form it probably won't answer all your sorting questions right now, but it might be a good starting point for others.
Best effort so far uses following helper class ObservableGroupingCollection
where the generic Grouping class (which itself exposes an ObservableCollection) comes from this article
http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping
To make working demo:-
From new UWP Blank application, add the above ObservableGroupingCollection class. Then add another class file in same namespace and add all following classes
Finally, edit MainPage as follows
HandleCollectionChanged method only handles Add/Remove so far and will break down if NotifyCollectionChangedEventArgs parameter contains multiple items (Existing ObservableCollection class only notifies changes one at a time)
So it works okay but it all feels kind of hacky.
Suggestions for improvement highly welcome.