Tesseract
Tesseract

Reputation: 180

WPF - Grouped CollectionView showing multiple times same group


I'm having a problem with grouping a CollectionView which is bound to an ItemsControl.

In my ViewModel I have multiple groups. A Group is a collection of Person, and each Person knows which group it belongs to.
See Group and Person classes at the end.

- Here's what I want to do :
I want to be able to show a collection of Person(s) grouped by their groups. I emphesize that I want to create these groups in the view from a collection of Persons, not from a collection of Groups.

- Here's how I do it :
I have a list which contains every Person across all groups (the list is called "Everyone"), and I have a CollectionView which I create from Everyone called "EveryoneGrouped". I then add a PropertyGroupDescription created from "Group" to my EveryoneGrouped collection.

- Here's my problem :
Groups appear multiple times, in fact groups appear as many times as the amount of Person they contain.
If I have the following groups :
- Group ONE ["Alpha One"]
- Group TWO ["Alpha Two", "Beta Two"]
- Group THREE ["Alpha Three", "Beta Three", "Gamma Three"]
It will produce the following : Application Screenshot
Group ONE appears only once since it contains only one Person. I would like for groups TWO and THREE to appear only once as well.
I just can't put my finger on what I'm doing wrong, any help would be greatly appreciated.

Edit : multiple Groups or Persons can have the same name.

Here's my code which produces the previous screenshot :

ViewModel

public class ViewModel
{
    public CollectionView EveryoneGrouped { get; private set; }
    private List<Person> Everyone { get; set; } = new List<Person>();
    private List<Group> AllGroups { get; set; } = new List<Group>();

    public ViewModel()
    {
        populateGroups();
        populateEveryoneCollection();
        createCollectionView();
    }

    private void populateGroups()
    {
        Group one = new Group
        {
            new Person { PersonName = "Alpha One" }
        };
        one.GroupName = "ONE";

        Group two = new Group
        {
            new Person { PersonName = "Alpha Two" },
            new Person { PersonName = "Beta Two" }
        };
        two.GroupName = "TWO";

        Group three = new Group
        {
            new Person { PersonName = "Alpha Three" },
            new Person { PersonName = "Beta Three" },
            new Person { PersonName = "Gamma Three" }
        };
        three.GroupName = "THREE";

        AllGroups.Add(one);
        AllGroups.Add(two);
        AllGroups.Add(three);
    }

    private void populateEveryoneCollection()
    {
        foreach(Group group in AllGroups)
        {
            foreach(Person person in group)
            {
                Everyone.Add(person);
            }
        }
    }

    private void createCollectionView()
    {
        EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
        PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group");
        EveryoneGrouped.GroupDescriptions.Add(groupDescription);
    }
}

XAML

<Window x:Class="FunWithCollectionViewGrouping.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:FunWithCollectionViewGrouping"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <ItemsControl ItemsSource="{Binding EveryoneGrouped}">

        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>

        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding PersonName}" Margin="5,0"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>

        <ItemsControl.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Name.Group.GroupName}" />
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ItemsControl.GroupStyle>
    </ItemsControl>
</Grid>

Code Behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }
}

Person

public class Person
{
    private static int PersonQuantity = 0;
    private int _id = ++PersonQuantity;
    public int Id { get { return _id; } }
    public Group Group { get; set; }
    public string PersonName { get; set; }
}

Group

public class Group : ObservableCollection<Person>
{
    private static int GroupQuantity = 0;
    private int _id = ++GroupQuantity;
    public int Id { get { return _id; } }
    public string GroupName { get; set; }

    public Group()
    {
        CollectionChanged += Group_CollectionChanged;
    }

    private void Group_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        if( e.NewItems != null )
        {
            foreach(Person p in e.NewItems)
            {
                if ( p.Group != null ) { p.Group.Remove(p); }
                p.Group = this;
            }
        }

        if( e.OldItems != null )
        {
            foreach (Person p in e.OldItems)
            {
                if (p.Group != null && p.Group.Equals(this)) { p.Group = null; }
            }
        }
    }

    public override bool Equals(object obj)
    {
        Group other = obj as Group;
        if (obj == null) return false;
        return Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id;
    }
}

Thank you.

Upvotes: 1

Views: 1324

Answers (1)

mm8
mm8

Reputation: 169280

Group by the GroupName property of the Group:

private void createCollectionView()
{
    EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
    PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group.GroupName");
    EveryoneGrouped.GroupDescriptions.Add(groupDescription);
}

And display the name of the group:

<ItemsControl.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Name}" />
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</ItemsControl.GroupStyle>

The problem I have with this solution is that multiple groups can have the same name (I updated my post to specify this), and they will be merged in the view if I do this. Groups are distinguished by their ID (which I can't sort by because then I would loose the group name in the view).

Group by the Id property then:

private void createCollectionView()
{
    EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
    PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group.Id");
    EveryoneGrouped.GroupDescriptions.Add(groupDescription);
}

And bind the TextBlock to the GroupName property of the Group of the first item in the group:

<ItemsControl.GroupStyle>
    <GroupStyle>
        <GroupStyle.HeaderTemplate>
            <DataTemplate>
                <TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Items[0].Group.GroupName}" />
            </DataTemplate>
        </GroupStyle.HeaderTemplate>
    </GroupStyle>
</ItemsControl.GroupStyle>

What I don't understand is why it behaves this way, doesn't PropertyGroupDescription use Equals and GetHashCode?

Obviously not. It uses reflection to be able to group by any property specified by a string.

Upvotes: 4

Related Questions