This question is based on:
ObservableGroupedCollection<TKey, TElement>
class from the NuGet package "CommunityToolkit.Mvvm" by MicrosoftWhile 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.
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 |
A | B | E | F | W |
---|---|---|---|---|
a_2 | b_0 | e_0 | f_0 | w_1 |
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
.
The following code is a quick and dirty example application to illustrate my problem and to provide a foundation for possible participants.
Requirements:
namespace "yourRootNamespace".Models;
public class SomeModel
{
public string SomeString { get; set; }
public SomeModel(string _s)
{
SomeString = _s;
}
}
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));
}
}
using "yourRootNamespace".ViewModels;
using System.Windows;
namespace "yourRootNamespace";
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
}
<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>
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.