Josef Nemec
Josef Nemec

Reputation: 689

WPF ListCollectionView custom grouping/sorting by List<string>

My Setup

I'm using ListCollectionView to sort and group my data in ListBox.

My model:

public class Game
{
    public string Name
    {
        get; set;
    }

    public List<string> Categories
    {
        get; set;
    }
}

What I want do to is to group by Categories, where groups in view are sorted alphabetically. And also sort all items in groups by Name.

Also I want to have group representing null or empty Categories at bottom of view.

For example:

Category1
    Name2
    Name3
Category2
    Name2
    Name3
    Name4
    Name5
Null/Empty
    Name1
    Name6

Since Categories is List<string> it means that any item can be in one or more groups or none.

Problem

I don't know how to properly implement sorting for categories. I know that I have to set multiple SortDescriptions to sort by Categories first and by Name second, but how do I do that?

WPF doesn't known how to sort List<string> groups by default so I tried to change it to custom List which implements IComparable, but that can't handle null values (comparer cannot be called if one of the objects is null).

I also tried to implement CustomSort on my view, but I thinks it's not impossible to handle my situation that way (where one item can be in multiple groups).

Update full repro code

The result is:

Null
    Game1
Category2
    Game2
    Game3
Category1
    Game3
    Game4

And I want it to be:

Category1
    Game3
    Game4
Category2
    Game2
    Game3
Null
    Game1

Code:

public partial class MainWindow : Window
{
    public class Game
    {
        public string Name
        {
            get; set;
        }

        public List<string> Categories
        {
            get; set;
        }
    }

    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {
        var games = new List<Game>()
        {
            new Game()
            {
                Name = "Game1",
            },
            new Game()
            {
                Name = "Game2",
                Categories = new List<string>() { "Category2" }                    
            },
            new Game()
            {
                Name = "Game3",
                Categories = new List<string>() { "Category1", "Category2" }
            },
            new Game()
            {
                Name = "Game4",
                Categories = new List<string>() { "Category1" }
            }
        };

        var view = (ListCollectionView)CollectionViewSource.GetDefaultView(games);
        view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
        view.GroupDescriptions.Add(new PropertyGroupDescription("Categories"));
        MainList.ItemsSource = view;
    }
}

And XAML:

<Window x:Class="grouping.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:grouping"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <Grid>
        <ListBox Name="MainList">
            <ListBox.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate>
                                        <Expander Header="{Binding Name, TargetNullValue='Null'}" IsExpanded="True">
                                            <ItemsPresenter />
                                        </Expander>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </ListBox.GroupStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

Upvotes: 0

Views: 1820

Answers (1)

grek40
grek40

Reputation: 13438

Basically, your data are not in suitable format for displaying in WPF. You want the same entry to appear in multiple places (once per group), while the typical itemssource will display each item once, no matter the sort details.

So you should translate your data to a representation that is more natural to WPF. For example (with custom comparer for sorting):

public class GameListEntry : IComparable<GameListEntry>
{
    public string Name { get; set; }
    public string Category { get; set; }

    public int CompareTo(GameListEntry other)
    {
        if (Category == other.Category)
        {
            return 0;
        }
        if (Category == "Null / Empty")
        {
            return 1;
        }
        if (other.Category == "Null / Empty")
        {
            return -1;
        }
        return Category.CompareTo(other.Category);
    }
}


List<Game> games = ...;

var listedGameEntries = games.SelectMany(x => x.Categories.DefaultIfEmpty().Select(c => new GameListEntry { Name = x.Name, Category = string.IsNullOrEmpty(c) ? "Null / Empty" : c }));

var groupedGames = listedGameEntries.GroupBy(x => x.Category);

The CompareTo might need to be enhanced or replaced by a different mechanism in order to make the sorting and grouping work the way it should.

Edit: added DefaultIfEmpty() in order to ensure one entry even with an empty list of categories.

Upvotes: 2

Related Questions