Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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>
andReadOnlyObservableCollection<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>
andIReadOnlyObservableGroup<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>
andReadOnlyObservableCollection<T>
, so just likeObservableGroup<TKey, TElement>
andReadOnlyObservableGroup<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:
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.
MVVM Toolkit