首页 > 解决方案 > 当分组属性是字符串列表时,按组名对 ListView 中的分组进行排序

问题描述

我正在开发由其他人开发的 C# (MVVM) WPF 应用程序。

在此应用程序中,对 ListView 中的字符串属性列表进行了一些分组,此属性上的分组似乎正在工作,但问题是这些组未按ListView 中的组名字母顺序排序。

例如 :

CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(AvailablePackages);

PropertyGroupDescription groupDescription = new PropertyGroupDescription("SupportedOperatingSystems");
view.GroupDescriptions.Add(groupDescription);
view.SortDescriptions.Add(new SortDescription("SupportedOperatingSystems", ListSortDirection.Ascending));

属性“SupportedOperatingSystems”是一个字符串列表

该行:view.SortDescriptions.Add(new SortDescription("SupportedOperatingSystems", ListSortDirection.Ascending)); 正在引发异常: System.InvalidOperationException:“无法比较数组中的两个元素。”

感谢您的帮助:)

标签: c#wpfsortingmvvmgrouping

解决方案


SortDescription 仅指定有关排序所依据的属性和排序顺序的信息。
使用此信息,CollectionView 按此属性对项目进行排序。但是对于排序,用于 sorted 属性的类型必须实现 IComparable 接口。
如果此接口不存在,则会引发您指定的错误。
据我所知,没有.Net 集合具有此接口的实现。
为了解决这个问题,你需要将原始集合的元素反射到一个额外的类型中给 View。
在此类型中,添加一个属性,原始元素的集合将反映在转换为 IComparable 实现的类型中。

不知道您的实现细节,我无法为您提供有关如何实现此功能的确切代码。
因此,我展示了一个非常简化的示例:

using System;
using System.Collections.Generic;

namespace SortDescriptionList
{
    public class ListItem
    {
        public IReadOnlyList<string> Strings { get; }
        public string ViewStrings => string.Join(", ", Strings);
        public ListItem(params string[] strings)
            => Strings = Array.AsReadOnly(strings);
    }
}
using System;
using System.Collections.Generic;

namespace SortDescriptionList
{
    public class StringCollection : List<string>, IComparable, IComparable<IReadOnlyList<string>>
    {
        public int CompareTo(IReadOnlyList<string> other)
        {
            if (other == null)
                return 1;

            var minCount = Count;
            if (minCount > other.Count)
                minCount = other.Count;

            if (minCount > 0)
            {
                for (int i = 0; i < minCount; i++)
                {
                    var comp = string.Compare(this[i], other[i]);
                    if (comp != 0)
                        return comp;
                }
            }
            return Count.CompareTo(other.Count);
        }

        public int CompareTo(object obj)
            => CompareTo(obj as IReadOnlyList<string>);
    }
}
namespace SortDescriptionList
{
    public class ListItemView
    {
        public StringCollection StringsView { get; }
        public string ViewStrings => Source.ViewStrings;
        public ListItem Source { get; }

        public ListItemView(ListItem source)
        {
            Source = source;
            StringsView = new StringCollection();
            StringsView.AddRange(source.Strings);
        }
    }
}
using System.Collections.Generic;
using System.Linq;

namespace SortDescriptionList
{

    public class ViewModel
    {
        public List<ListItem> ListItems { get; }
            = new List<ListItem>()
            {
                new ListItem("First"),
                new ListItem("Second"),
                new ListItem("Third"),
                new ListItem("Fourth", "Fifth")
            };

        public List<ListItemView> ListItemsView { get; }

        public ViewModel()
        {
            ListItemsView = new List<ListItemView>(ListItems.Select(item => new ListItemView(item)));
        }
    }
}
<Window x:Class="SortDescriptionList.SortListWindow"
        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:SortDescriptionList"
        mc:Ignorable="d"
        Title="SortListWindow" Height="450" Width="800">
    <FrameworkElement.DataContext>
        <local:ViewModel/>
    </FrameworkElement.DataContext>
    <UniformGrid Columns="2">
        <DataGrid x:Name="dataGrid"
                  ItemsSource="{Binding ListItems}"/>
        <DataGrid x:Name="dataGridView"
                  ItemsSource="{Binding ListItemsView}"/>
    </UniformGrid>
</Window>

左列显示带有原始元素的 DataGrid。
并且单击排序仅在“ViewStrings”列上可用,因为不可能为具有常规集合类型的“Strings”属性创建 SortDescription。

在反射集合的右列中,您可以启用按“StringsView”列排序。
由于此属性的类型不再是常规列表,而是实现 IComparable 接口的自定义 StringCollection,因此您可以为其创建 SortDescription。

补充答案

这是zip文件的项目

我无法准确判断在实践中需要多少,但是您需要的实现非常复杂且不灵活。
您可以按在其元素中 SupportedPlatforms 集合中指定的组对 OcAvailablePackages 集合的元素进行排序。

但是由于您需要的排序不是按 SupportedPlatforms 集合的元素顺序,而是按 SupportedPlatforms 元素的排序顺序,因此您还需要先对它们进行排序。
为了实现这样的排序,我不得不想出一个相当复杂的类型:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Test_Grouping
{
    public class CompareReadOnlyList<T> : ICollection, IReadOnlyList<T>, IComparable, IComparable<IReadOnlyList<T>>
        where T : IComparable, IComparable<T>
    {
        private readonly T[] array;
        public int Count => array.Length;

        public bool IsReadOnly => true;

        public object SyncRoot => null;
        public bool IsSynchronized => false;

        public T this[int index] => array[index];
        public CompareReadOnlyList(IEnumerable<T> ts)
        {
            array = ts.ToArray();
            Array.Sort(array);
        }

        public CompareReadOnlyList(params T[] ts)
        {
            array = (T[])ts.Clone();
            Array.Sort(array);
        }

        public int CompareTo(IReadOnlyList<T> other)
        {
            if (other == null)
                return 1;

            int minCount = Count;
            if (minCount > other.Count)
            {
                minCount = other.Count;
            }

            if (minCount > 0)
            {
                for (int i = 0; i < minCount; i++)
                {
                    int comp = Compare(this[i], other[i]);
                    if (comp != 0)
                    {
                        return comp;
                    }
                }
            }
            return Count.CompareTo(other.Count);
        }

        public int CompareTo(object obj)
        {
            return CompareTo(obj as IReadOnlyList<T>);
        }

        public int Compare(T x, T y)
        {
            return x?.CompareTo(y) ?? -(y?.CompareTo(x) ?? 0);
        }

        public IEnumerator<T> GetEnumerator()
        {
            return ((IEnumerable<T>)array).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return array.GetEnumerator();
        }

        public void CopyTo(Array array, int index)
        {
            throw new NotImplementedException();
        }
    }
}
using Infomil.ZSYS.WSUS.Mvvm;
using System.Collections.Generic;

namespace Test_Grouping
{
    public class Package : ViewModelBase
    {
        #region Properties

        private string _name;
        public string Name
        {
            get => _name;
            set => Set(ref _name, value);
        }

        private CompareReadOnlyList<string> _supportedPlatforms ;
        public CompareReadOnlyList<string> SupportedPlatforms
        {
            get => _supportedPlatforms;
            set => Set(ref _supportedPlatforms, value);
        }

        #endregion

        #region Constructors
        public Package() { }
        public Package(string name) => Name = name;
        public Package(string name, IEnumerable<string> vs) 
            : this(name)
            => SupportedPlatforms = new CompareReadOnlyList<string>(vs);
        public Package(string name, params string[] vs)
            : this(name, (IEnumerable<string>)vs)
        { }
        #endregion

        #region Methods

        #endregion
    }
}
using Infomil.ZSYS.WSUS.Mvvm;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;

namespace Test_Grouping
{
    public class MainWindowVM : ViewModelBase
    {
        #region Properties

        private ObservableCollection<Package> _ocAvailablePackages = new ObservableCollection<Package>();
        public ObservableCollection<Package> OcAvailablePackages
        {
            get => _ocAvailablePackages;
            set => Set(ref _ocAvailablePackages, value);
        }

        private Package _objSelectedPackage;
        public Package ObjSelectedPackage
        {
            get => _objSelectedPackage;
            set => Set(ref _objSelectedPackage, value);
        }

        #endregion

        #region Constructors
        public MainWindowVM()
        {
            if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
            {
                return;
            }

            //MessageBox.Show("test");

            OcAvailablePackages.Add(new Package("Package 1", "Platform 2"));

            OcAvailablePackages.Add(new Package("Package 2", "Platform 2", "Platform 4"));

            OcAvailablePackages.Add(new Package("Package 3", "Platform 2", "Platform 1", "Platform 3"));

            OcAvailablePackages.Add(new Package("Package 4", "Platform 3", "Platform 1", "Platform 4"));

            OcAvailablePackages.Add(new Package("Package 5", "Platform 2", "Platform 3"));

            OcAvailablePackages.Add(new Package("Package 6", "Platform 4", "Platform 1", "Platform 3"));

            //Group
            ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(OcAvailablePackages);
            view.GroupDescriptions.Add(new PropertyGroupDescription(nameof(Package.SupportedPlatforms)));

            //The next code row raises an exception (due to List<string>) :
            view.SortDescriptions.Add(new SortDescription(nameof(Package.SupportedPlatforms), ListSortDirection.Ascending));

        }
        #endregion

        #region Methods

        #endregion
    }

}

但我真的不喜欢这个实现。
首先,我不确定它是否总是能正常工作。
我还没有想出一个价值观的组合来支持这个假设,但我认为是的。
其次,这不会对组内的项目进行排序。
由于首先对一般集合进行排序,然后将其按元素集合中明确指定的组进行分组,因此这些组是虚拟的。
虽然 ListView 显示了两打行,但实际上有六个项目,所以仍然保持相同的数量。

我将通过一个额外的类型来实现,该类型表示一个包,并明确细分为每个组的单独元素:

using Infomil.ZSYS.WSUS.Mvvm;
using System.Collections.Generic;
using System.Linq;

namespace Test_Grouping
{
    public class PackageRow : ViewModelBase
    {
        public Package Package { get; }

        public string Platform => Package.SupportedPlatforms[index];
        private readonly int index;

        public string Name => Package.Name;

        private PackageRow(Package package, string platform)
        {
            Package = package;
            index = package.SupportedPlatforms.TakeWhile(p => p != platform).Count();
        }

        public static IEnumerable<PackageRow> CreatePackageRows(Package package)
            => package.SupportedPlatforms.Select(platform => new PackageRow(package, platform));

        public static void AddInList(IList<PackageRow> packages, Package package)
        {
            foreach (var pv in CreatePackageRows(package))
            {
                packages.Add(pv);
            }
        }

        public static void RemoveInList(IList<PackageRow> packages, Package package)
        {
            for (int i = packages.Count - 1; i >= 0; i--)
            {
                if (packages[i].Package == package)
                {
                    packages.RemoveAt(i);
                }
            }
        }

    }
}
            foreach (var pck in OcAvailablePackages)
            {
                PackageRow.AddInList(Packages, pck);
            }

            //Group
            ListCollectionView viewGr = (ListCollectionView)CollectionViewSource.GetDefaultView(Packages);
            viewGr.GroupDescriptions.Add(new PropertyGroupDescription(nameof(PackageRow.Platform)));

            //The next code row raises an exception (due to List<string>) :
            viewGr.SortDescriptions.Add(new SortDescription(nameof(PackageRow.Platform), ListSortDirection.Ascending));
            viewGr.SortDescriptions.Add(new SortDescription(nameof(PackageRow.Name), ListSortDirection.Ascending));


        }
        public ObservableCollection<PackageRow> Packages { get; }
           = new ObservableCollection<PackageRow>();

存档更改:Test_Grouping(Le Zvince).7z

应用程序的屏幕截图。 左列是使用属性类型中的自定义比较器实现的输出。
对 - 将原始元素分成几个,每个组(平台)一个。在这种情况下,您不需要修改原来的 Package 类型。

在此处输入图像描述


推荐阅读