Search code examples
c#.netwpfxamlcommunity-toolkit-mvvm

Proper way of displaying an ObservableGroupedCollection<string, TElement> using Wpf .NET 6 and the CommunityToolkit.Mvvm Package


ObservableGroupedCollection in Wpf .NET 6

This question is based on:

  • A Wpf project using .NET 6
  • The ObservableGroupedCollection<TKey, TElement> class from the NuGet package "CommunityToolkit.Mvvm" by Microsoft
  • striktly obeying the MVVM pattern

While tinkering with the relatively new CommunityToolkit.Mvvm I came across the ObservableGroupedCollection<TKey, TElement> class which in Wpf is rather undocumented. My Wpf knowledge is poor at best - I intended to use this as a learning project - and I was unable to transfer the existing UWP xaml code into a working Wpf sample application.
The sample app referenced in the blog post above uses a CollectionViewSource bound to an ObservableGroupedCollection<TKey, TElement> to display a grouped list of contacts within a scrollable control. My attempts to replicate this behavior within a Wpf .NET 6 app resulted in only the first values of each collection being displayed, rather than the entire range.

What is the proper way to display all entries in a grouped fashion, while obeying to the MVVM pattern?!

The following image shows an excerpt from the Microsoft Store sample application on the left and the desired result on the right.

SampleApp vs Desired

Results from the sample code below

Results when iterating manually through the groups and their collections:

A B E F W
a_2 b_0 e_0 f_0 w_1
a_1 f_1 w_0
a_0 f_2

Values displayed in the actual ListView:

A B E F W
a_2 b_0 e_0 f_0 w_1

Displayed values These are obviously values that got scraped off the "top" of the collections.

What puzzles me is the fact that the SemanticZoom used in the original Sample App (.xaml - UWP) and the corresponding ViewModel.cs is somehow able to display ALL entries instead of scraping off the first element of the collection. While still using a model based DataTemplate.

Sample code

The following code is a quick and dirty example application to illustrate my problem and to provide a foundation for possible participants.

Requirements:

  • Wpf Project -> .NET 6
  • NuGet package: CommunityToolkit.Mvvm by Microsoft
  • 2 new folders: Models and ViewModels
  • Replace all instances of "yourRootNamespace" with your actual root namespace

SomeModel.cs

namespace "yourRootNamespace".Models;

public class SomeModel
{

    public string SomeString { get; set; }

    public SomeModel(string _s)
    {
        SomeString = _s;
    }
}

MainWindowViewModel.cs

using CommunityToolkit.Mvvm.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using "yourRootNamespace".Models;
using System.Collections.Generic;
using System.Linq;

namespace "yourRootNamespace".ViewModels;

public partial class MainWindowViewModel : ObservableObject
{

    [ObservableProperty]
    private ObservableGroupedCollection<string, SomeModel>? m_someObservableGroupedCollection;

    public MainWindowViewModel()
    {
        List<SomeModel> tempList = new List<SomeModel>()
        {
            new SomeModel("w_1"),
            new SomeModel("b_0"),
            new SomeModel("a_2"),
            new SomeModel("e_0"),
            new SomeModel("f_0"),
            new SomeModel("f_1"),
            new SomeModel("a_1"),
            new SomeModel("a_0"),
            new SomeModel("w_0"),
            new SomeModel("f_2")
        };

        m_someObservableGroupedCollection = new ObservableGroupedCollection<string, SomeModel>(tempList
            .GroupBy(c => char.ToUpperInvariant(c.SomeString[0]).ToString())
            .OrderBy(g => g.Key));
    }
}

MainWindow.xaml.cs

using "yourRootNamespace".ViewModels;
using System.Windows;

namespace "yourRootNamespace";

public partial class MainWindow : Window
{

    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainWindowViewModel();
    }
}

MainWindow.xaml

<Window x:Class=""yourRootNamespace".MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:"yourRootNamespace""
        xmlns:collections="clr-namespace:CommunityToolkit.Mvvm.Collections;assembly=CommunityToolkit.Mvvm"
        xmlns:viewmodels="clr-namespace:"yourRootNamespace".ViewModels"
        xmlns:models="clr-namespace:"yourRootNamespace".Models"
        d:DataContext="{d:DesignInstance Type=viewmodels:MainWindowViewModel}"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

    <Window.Resources>

        <CollectionViewSource
            x:Key="SomeListViewSource"
            Source="{Binding SomeObservableGroupedCollection}"
            IsLiveGroupingRequested="True"/>

        <DataTemplate
            x:Key="SomeTemplate"
            DataType="{x:Type models:SomeModel}">
            <TextBlock Text="{Binding SomeString}"/>
        </DataTemplate>

    </Window.Resources>

    <Grid>
        <ListView
            ItemTemplate="{StaticResource SomeTemplate}"
            ItemsSource="{Binding Source={StaticResource SomeListViewSource}, Mode=OneWay}"
            SelectionMode="Single">
            <ListView.GroupStyle>
                <GroupStyle
                    HidesIfEmpty="True">
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate
                            DataType="{x:Type collections:IReadOnlyObservableGroup}">
                            <TextBlock Text="{Binding Key}"/>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>
    </Grid>

</Window>

Solution

  • I think there is difference between WPF and UWP, or i don't know where, my UI platform knowledge is poor: CollectionViewSource in dotnet 6 WPF does not have IsSourceGrouped property. Code in Microsoft samples sets this property to true, but your code does not - because WPF doesn't provide it.

    I was getting same behavior and went to compare what's different, line by line and found the absense of IsSourceGrouped. The only explanation i can think of, is that grouped collection is a collection of nested collections, so internals of ListView/CollectionViewSource have to properly enumerate it in-depth, and that's what is not happening in your case.

    So the answer would be: this feature is missing in a framework version you are using, so you have to resort to workarounds, and not strictly clean MVVM.

    UPD: I managed to do clean xaml-only grouping with only an ObservableCollection<T> in ViewModel. ListView gets groups and items from a ItemsSource="{Binding Source={StaticResource CustomViewSource}} which controls how to group, sort and so on:

    <CollectionViewSource x:Key="CustomViewSource" Source="{Binding OnlineMods}" IsLiveGroupingRequested="True">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="YourPropertyForGrouping" />
        </CollectionViewSource.GroupDescriptions>
        <CollectionViewSource.SortDescriptions>
            <componentModel:SortDescription PropertyName="YourPropertyForOrdering" />
        </CollectionViewSource.SortDescriptions>
    </CollectionViewSource>
    

    It is also possible to add predefined empty groups there to be always displayed and do other tricks.