Edit

Share via


Observable grouped collection APIs

The MVVM Toolkit features several observable grouped collection and a group of helper APIs, to facilitate working with grouped collection of items that can then be bound to the UI. This can be useful when constructing UIs such as grouped lists of contacts, or any kind of grouped collection of items that the user can then interact with.

Platform APIs: ObservableGroup<TKey, TElement>, ReadOnlyObservableGroup<TKey, TElement>, IReadOnlyObservableGroup, IReadOnlyObservableGroup, ObservableGroupedCollection<TKey, TElement>, ReadOnlyObservableGroupedCollection<TKey, TElement>, ObservableGroupedCollectionExtensions.

ObservableGroup<TKey, TElement> and ReadOnlyObservableGroup<TKey, TElement>

The ObservableGroup<TKey, TElement> and ReadOnlyObservableGroup<TKey, TElement> types are custom observable collection types inheriting from ObservableCollection<T> and ReadOnlyObservableCollection<T> that also provide grouping support. This is particularly useful when binding a grouped collection of items to the UI, such as to display a list of contacts.

ObservableGroup<TKey, TElement> features

ObservableGroup<TKey, TElement> and ReadOnlyObservableGroup<TKey, TElement> have the following main features:

  • They inherit from ObservableCollection<T> and ReadOnlyObservableCollection<T>, thus providing the same notification support for when items are added, removed or modified in the collection.
  • They also implement the IGrouping<TKey, TElement> interface, allowing instances to be used as arguments for all existing LINQ extensions working on instances of this interface.
  • They implement several interfaces from the MVVM Toolkit (IReadOnlyObservableGroup, IReadOnlyObservableGroup<TKey> and IReadOnlyObservableGroup<TKey, TElement>) that enable different level of abstraction over instances of these two collection types. This can be especially useful in data templates, where only partial type information is available or can be used.

ObservableGroupedCollection<TKey, TElement> and ReadOnlyObservableGroupedCollection<TKey, TElement>

The ObservableGroupedCollection<TKey, TElement> and ReadOnlyObservableGroupedCollection<TKey, TElement> are observable collection types where each item is a grouped collection type (either ObservableGroup<TKey, TElement> or ReadOnlyObservableGroup<TKey, TElement>), that also implement ILookup<TKey, TElement>.

ObservableGroupedCollection<TKey, TElement> features

ObservableGroupedCollection<TKey, TElement> and ReadOnlyObservableGroupedCollection<TKey, TElement> have the following main features:

  • They inherit from ObservableCollection<T> and ReadOnlyObservableCollection<T>, so just like ObservableGroup<TKey, TElement> and ReadOnlyObservableGroup<TKey, TElement> they also provide notification when items (groups, in this case) are added, removed or modified.
  • They implement the ILookup<TKey, TElement> interface, to improve LINQ interoperability.
  • They provide additional helper methods to easily interact with groups and items in these collection through all the APIs in the ObservableGroupedCollectionExtensions type.

Working with ObservableGroupedCollection<TKey, TElement>

Suppose we had a simple definition of a Contact model like this:

public sealed record Contact(Name Name, string Email, Picture Picture);

public sealed record Name(string First, string Last)
{
    public override string ToString() => $"{First} {Last}";
}

public sealed record Picture(string Url);

And then a viewmodel using ObservableGroupedCollection<TKey, TElement> to setup a list of contacts:

public class ContactsViewModel : ObservableObject
{
    /// <summary>
    /// The <see cref="IContactsService"/> instance currently in use.
    /// </summary>
    private readonly IContactsService contactsService;

    public ContactsViewModel(IContactsService contactsService) 
    {
        this.contactsService = contactsService;

        LoadContactsCommand = new AsyncRelayCommand(LoadContactsAsync);
    }

    public IAsyncRelayCommand LoadContactsCommand { get; }

    /// <summary>
    /// Gets the current collection of contacts
    /// </summary>
    public ObservableGroupedCollection<string, Contact> Contacts { get; private set; } = new();

    /// <summary>
    /// Loads the contacts to display.
    /// </summary>
    private async Task LoadContactsAsync()
    {
        var contacts = await contactsService.GetContactsAsync();

        Contacts = new ObservableGroupedCollection<string, Contact>(
            contacts.Contacts
            .GroupBy(static c => char.ToUpperInvariant(c.Name.First[0]).ToString())
            .OrderBy(static g => g.Key));

        OnPropertyChanged(nameof(Contacts));
    }
}

And the relative UI could then be (using WinUI XAML):

<UserControl
    x:Class="MvvmSampleUwp.Views.ContactsView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
    xmlns:contacts="using:MvvmSample.Core.Models"
    xmlns:core="using:Microsoft.Xaml.Interactions.Core"
    xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
    xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
    xmlns:system="using:System"
    NavigationCacheMode="Enabled">
    <Page.Resources>

        <!--  SemanticZoom grouped source (this connects the grouped collection to the UI)  -->
        <CollectionViewSource
            x:Name="PeopleViewSource"
            IsSourceGrouped="True"
            Source="{x:Bind ViewModel.Contacts, Mode=OneWay}" />

        <!--  Contact template  -->
        <DataTemplate x:Key="PersonListViewTemplate" x:DataType="contacts:Contact">

            <!-- Some custom template here -->
        </DataTemplate>
    </Page.Resources>

    <!--  A command to load contacts when the control is loaded in the visual tree  -->
    <interactivity:Interaction.Behaviors>
        <core:EventTriggerBehavior EventName="Loaded">
            <core:InvokeCommandAction Command="{x:Bind ViewModel.LoadContactsCommand}" />
        </core:EventTriggerBehavior>
    </interactivity:Interaction.Behaviors>

    <Grid>

        <!--  Loading bar (is displayed when the contacts are loading)  -->
        <muxc:ProgressBar
            HorizontalAlignment="Stretch"
            VerticalAlignment="Top"
            Background="Transparent"
            IsIndeterminate="{x:Bind ViewModel.LoadContactsCommand.IsRunning, Mode=OneWay}" />

        <!--  Contacts view  -->
        <SemanticZoom>
            <SemanticZoom.ZoomedInView>
                <ListView
                    ItemTemplate="{StaticResource PersonListViewTemplate}"
                    ItemsSource="{x:Bind PeopleViewSource.View, Mode=OneWay}"
                    SelectionMode="Single">
                    <ListView.GroupStyle>
                        <GroupStyle HidesIfEmpty="True">

                            <!--  Header template (you can see IReadOnlyObservableGroup being used here)  -->
                            <GroupStyle.HeaderTemplate>
                                <DataTemplate x:DataType="collections:IReadOnlyObservableGroup">
                                    <TextBlock
                                        FontSize="24"
                                        Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
                                        Text="{x:Bind Key}" />
                                </DataTemplate>
                            </GroupStyle.HeaderTemplate>
                        </GroupStyle>
                    </ListView.GroupStyle>
                </ListView>
            </SemanticZoom.ZoomedInView>
            <SemanticZoom.ZoomedOutView>
                <GridView
                    HorizontalAlignment="Stretch"
                    ItemsSource="{x:Bind PeopleViewSource.View.CollectionGroups, Mode=OneWay}"
                    SelectionMode="Single">
                    <GridView.ItemTemplate>

                        <!--  Zoomed out header template (IReadOnlyObservableGroup is used again)  -->
                        <DataTemplate x:DataType="ICollectionViewGroup">
                            <Border Width="80" Height="80">
                                <TextBlock
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Center"
                                    FontSize="32"
                                    Foreground="{ThemeResource SystemControlHighlightAccentBrush}"
                                    Text="{x:Bind Group.(collections:IReadOnlyObservableGroup.Key)}" />
                            </Border>
                        </DataTemplate>
                    </GridView.ItemTemplate>
                </GridView>
            </SemanticZoom.ZoomedOutView>
        </SemanticZoom>
    </Grid>
</UserControl>

This would display a grouped list of contacts, with each group using the initial of its contained contacts as title.

When run, the snippet above results in the following UI, from the MVVM Toolkit Sample App:

Contacts view sample

Examples

  • Check out the sample app (for multiple UI frameworks) to see the MVVM Toolkit in action.
  • You can also find more examples in the unit tests.