You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PropertyGroupDescription自定义排序时CollectionViewGroup的ItemCount始终为0

问题:WPF DataGrid分组按数量排序时CollectionViewGroup.ItemCount始终为0

前置说明

已检索StackOverflow,所有关于分组DataGrid排序的主题均不适用于此问题,目前没有不借助第三方库实现按分组数量排序的公开解决方案。

问题描述

尝试通过重写PropertyGroupDescription的CustomSort属性,实现DataGrid分组按数量排序。但为GroupDescription分配CustomSort方法后,Compare函数中的CollectionViewGroup对象的ItemCount始终为0,导致排序逻辑失效。

示例XAML代码

<Window 
    x:Class="GroupedSorting.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:GroupedSorting"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>

        <DataGrid
            Name="dg"
            Grid.Row="0"
            ItemsSource="{Binding ItemVMs}">

            <DataGrid.GroupStyle>
                <!-- Style for groups at top level. -->
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Margin" Value="0,0,0,5"/>
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type GroupItem}">
                                        <Expander IsExpanded="True" Background="#FF112255" BorderBrush="#FF002255" Foreground="#FFEEEEEE" BorderThickness="1,1,1,5">
                                            <Expander.Header>
                                                <DockPanel>
                                                    <TextBlock FontWeight="Bold" Text="{Binding Path=Name}" Margin="5,0,0,0" Width="100"/>
                                                    <TextBlock FontWeight="Bold" Text="{Binding Path=ItemCount}"/>
                                                </DockPanel>
                                            </Expander.Header>
                                            <Expander.Content>
                                                <ItemsPresenter />
                                            </Expander.Content>
                                        </Expander>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
                <!-- Style for groups under the top level. -->
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <DockPanel Background="LightBlue">
                                <TextBlock Text="{Binding Path=Name, Converter={StaticResource completeConverter}}" Foreground="Blue" Margin="30,0,0,0" Width="100"/>
                                <TextBlock Text="{Binding Path=ItemCount}" Foreground="Blue"/>
                            </DockPanel>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </DataGrid.GroupStyle>
            <DataGrid.RowStyle>
                <Style TargetType="DataGridRow">
                    <Setter Property="Foreground" Value="Black" />
                    <Setter Property="Background" Value="White" />
                </Style>
            </DataGrid.RowStyle>
        </DataGrid>
    </Grid>
</Window>

对应后台代码

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Data;

namespace GroupedSorting
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<MyViewModel> ItemVMs { get; set; } = new ObservableCollection<MyViewModel>();

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            var r = new Random();

            for (int i = 0; i < 10; i++)
            {
                ItemVMs.Add(new MyViewModel()
                {
                    Name = "Group 1",
                    MyIndex = r.Next()
                }); ;
            }

            for (int i = 0; i < 5; i++)
            {
                ItemVMs.Add(new MyViewModel()
                {
                    Name = "Group 2",
                    MyIndex = r.Next()
                });
            }

            for (int i = 0; i < 1; i++)
            {
                ItemVMs.Add(new MyViewModel()
                {
                    Name = "Group 3",
                    MyIndex = r.Next()
                });
            }

            List<MyViewModel> sortedItems = new List<MyViewModel>();
            var groups = ItemVMs.GroupBy(x => x.Name);
            foreach (var group in groups.OrderByDescending(x => x.Count()))
            {
                sortedItems.AddRange(group);
            }
            ItemVMs.Clear();
            ItemVMs = new ObservableCollection<MyViewModel>(sortedItems);


            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var sourceView = CollectionViewSource.GetDefaultView(dg.ItemsSource) as ListCollectionView;
            var gd = new PropertyGroupDescription(nameof(MyViewModel.Name));
            gd.CustomSort = new GroupComparer();
            sourceView.GroupDescriptions.Add(gd);
            sourceView.Refresh();
        }
    }

    public class MyViewModel
    {
        public string Name { get; set; }
        public int MyIndex { get; set; }
    }

    public class GroupComparer : System.Collections.IComparer
    {
        public int Compare(object x, object y)
        {
            if (!(x is CollectionViewGroup xViewGroup))
                return 0;

            if (!(y is CollectionViewGroup yViewGroup))
                return 0;

            Debug.WriteLine($"{xViewGroup.Name} {xViewGroup.ItemCount}, {yViewGroup.Name} {yViewGroup.ItemCount}");

            if (xViewGroup.ItemCount < yViewGroup.ItemCount)
                return 1;
            else if (xViewGroup.ItemCount > yViewGroup.ItemCount)
                return -1;

            return 0;
        }
    }
}
解决方案

问题根源

PropertyGroupDescription的CustomSort执行时机早于WPF内部填充CollectionViewGroup.ItemCount的阶段,此时无法通过该属性获取有效分组数量。

实现方案

基础版:直接从数据源统计数量

修改GroupComparer,让它持有数据源引用,在比较时直接从数据源统计对应分组的条目数:

public class GroupComparer : System.Collections.IComparer
{
    private readonly IEnumerable<MyViewModel> _sourceData;

    public GroupComparer(IEnumerable<MyViewModel> sourceData)
    {
        _sourceData = sourceData;
    }

    public int Compare(object x, object y)
    {
        if (!(x is CollectionViewGroup xViewGroup) || string.IsNullOrEmpty(xViewGroup.Name?.ToString()))
            return 0;

        if (!(y is CollectionViewGroup yViewGroup) || string.IsNullOrEmpty(yViewGroup.Name?.ToString()))
            return 0;

        int xCount = _sourceData.Count(item => item.Name == xViewGroup.Name.ToString());
        int yCount = _sourceData.Count(item => item.Name == yViewGroup.Name.ToString());

        return yCount.CompareTo(xCount); // 降序排列,如需升序则改为xCount.CompareTo(yCount)
    }
}

然后在初始化比较器时传入数据源:

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    var sourceView = CollectionViewSource.GetDefaultView(dg.ItemsSource) as ListCollectionView;
    var gd = new PropertyGroupDescription(nameof(MyViewModel.Name));
    gd.CustomSort = new GroupComparer(ItemVMs);
    sourceView.GroupDescriptions.Add(gd);
    sourceView.Refresh();
}

优化版:预缓存分组数量

如果数据量较大,每次比较都统计会影响性能,可提前预统计分组数量并缓存:

namespace GroupedSorting
{
    public partial class MainWindow : Window
    {
        public ObservableCollection<MyViewModel> ItemVMs { get; set; } = new ObservableCollection<MyViewModel>();
        private Dictionary<string, int> _groupCountCache;

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;

            // ... 原有填充数据的代码 ...

            // 预统计分组数量并缓存
            _groupCountCache = ItemVMs.GroupBy(x => x.Name)
                                     .ToDictionary(group => group.Key, group => group.Count());

            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var sourceView = CollectionViewSource.GetDefaultView(dg.ItemsSource) as ListCollectionView;
            var gd = new PropertyGroupDescription(nameof(MyViewModel.Name));
            gd.CustomSort = new GroupComparer(_groupCountCache);
            sourceView.GroupDescriptions.Add(gd);
            sourceView.Refresh();
        }
    }

    public class GroupComparer : System.Collections.IComparer
    {
        private readonly Dictionary<string, int> _groupCountCache;

        public GroupComparer(Dictionary<string, int> groupCountCache)
        {
            _groupCountCache = groupCountCache;
        }

        public int Compare(object x, object y)
        {
            if (!(x is CollectionViewGroup xViewGroup) || !_groupCountCache.TryGetValue(xViewGroup.Name.ToString(), out int xCount))
                return 0;

            if (!(y is CollectionViewGroup yViewGroup) || !_groupCountCache.TryGetValue(yViewGroup.Name.ToString(), out int yCount))
                return 0;

            return yCount.CompareTo(xCount); // 降序排列
        }
    }
}

内容的提问来源于stack exchange,提问作者Mike

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 14:20:34