UWP ObservableCollection排序和分组

Nic*_*ick 12 c# sorting listview win-universal-app uwp

在UWP应用程序中,如何对ObservableCollection进行分组和排序并保持所有实时通知的良好性?

在我看过的大多数简单的UWP示例中,通常有一个ViewModel公开一个ObservableCollection,然后绑定到View中的ListView.在ObservableCollection中添加或删除项目时,ListView会通过对INotifyCollectionChanged通知作出反应来自动反映更改.在未分类或未分组的ObservableCollection的情况下,这一切都可以正常工作,但如果需要对集合进行排序或分组,则似乎没有明显的方法来保留更新通知.更重要的是,动态更改排序或组顺序似乎会引发重大的实施问题.

++

假设您有一个现有的datacache后端,该后端公开了一个非常简单的类Contact的ObservableCollection.

public class Contact
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string State { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

此ObservableCollection随时间而变化,我们希望在视图中呈现实时分组和排序列表,以响应datacache中的更改进行更新.我们还希望为用户提供在运行时切换LastName和State之间的分组的选项.

++

在WPF世界中,这是相对微不足道的.我们可以创建一个简单的ViewModel,引用数据缓存,按原样显示缓存的Contacts集合.

public class WpfViewModel 
{
    public WpfViewModel()
    {
        _cache = GetCache();
    }

    Cache _cache;

    public ObservableCollection<Contact> Contacts
    {
        get { return _cache.Contacts; }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们可以将它绑定到一个视图,我们将CollectionViewSource和Sort和Group定义实现为XAML资源.

<Window .....
   xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase">

   <Window.DataContext>
      <local:WpfViewModel />
   </Window.DataContext>

    <Window.Resources>
        <CollectionViewSource x:Key="cvs" Source="{Binding Contacts}" />
        <PropertyGroupDescription x:Key="stategroup" PropertyName="State" />
        <PropertyGroupDescription x:Key="initialgroup" PropertyName="LastName[0]" />
        <scm:SortDescription x:Key="statesort" PropertyName="State" Direction="Ascending" />
        <scm:SortDescription x:Key="lastsort" PropertyName="LastName" Direction="Ascending" />
        <scm:SortDescription x:Key="firstsort" PropertyName="FirstName" Direction="Ascending" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ListView ItemsSource="{Binding Source={StaticResource cvs}}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="100" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <TextBlock Text="{Binding LastName}" />
                        <TextBlock Text="{Binding FirstName}" Grid.Column="1" />
                        <TextBlock Text="{Binding State}" Grid.Column="2" />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
            <ListView.GroupStyle>
                <GroupStyle>
                    <GroupStyle.HeaderTemplate>
                        <DataTemplate>
                            <Grid Background="Gainsboro">
                                <TextBlock FontWeight="Bold" 
                                           FontSize="14" 
                                           Margin="10,2"
                                           Text="{Binding Name}"/>
                            </Grid>
                        </DataTemplate>
                    </GroupStyle.HeaderTemplate>
                </GroupStyle>
            </ListView.GroupStyle>
        </ListView>

        <StackPanel Orientation="Horizontal" Grid.Row="1">
            <Button Content="Group By Initial" Click="InitialGroupClick" />
            <Button Content="Group By State" Click="StateGroupClick" />
        </StackPanel>

    </Grid>
</Window>
Run Code Online (Sandbox Code Playgroud)

然后当用户点击窗口底部的GroupBy按钮时,我们可以在代码隐藏中动态分组和排序.

private void InitialGroupClick(object sender, RoutedEventArgs e)
{
     var cvs = FindResource("cvs") as CollectionViewSource;
     var initialGroup = (PropertyGroupDescription)FindResource("initialgroup");
     var firstSort = (SortDescription)FindResource("firstsort");
     var lastSort = (SortDescription)FindResource("lastsort");

     using (cvs.DeferRefresh())
     {
         cvs.GroupDescriptions.Clear();
         cvs.SortDescriptions.Clear();
         cvs.GroupDescriptions.Add(initialGroup);
         cvs.SortDescriptions.Add(lastSort);
         cvs.SortDescriptions.Add(firstSort);
     }
}

private void StateGroupClick(object sender, RoutedEventArgs e)
{
     var cvs = FindResource("cvs") as CollectionViewSource;
     var stateGroup = (PropertyGroupDescription)FindResource("stategroup");
     var stateSort = (SortDescription)FindResource("statesort");
     var lastSort = (SortDescription)FindResource("lastsort");
     var firstSort = (SortDescription)FindResource("firstsort");

     using (cvs.DeferRefresh())
     {
         cvs.GroupDescriptions.Clear();
         cvs.SortDescriptions.Clear();
         cvs.GroupDescriptions.Add(stateGroup);
         cvs.SortDescriptions.Add(stateSort);
         cvs.SortDescriptions.Add(lastSort);
         cvs.SortDescriptions.Add(firstSort);
     }
}
Run Code Online (Sandbox Code Playgroud)

这一切都很好,并且数据缓存集合更改时项目会自动更新.Listview分组和选择不受集合更改的影响,并且新的联系人项目已正确分组.在运行时,用户可以在State和LastName之间交换分组.

++

在UWP世界中,CollectionViewSource不再具有GroupDescriptions和SortDescriptions集合,并且需要在ViewModel级别执行排序/分组.我找到的最接近可行解决方案的方法是微软的样本包

https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlListView

和这篇文章

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

其中ViewModel使用Linq对ObservableCollection进行分组,并将其作为分组项的ObservableCollection呈现给视图

public ObservableCollection<GroupInfoList> GroupedContacts
{
    ObservableCollection<GroupInfoList> groups = new ObservableCollection<GroupInfoList>();

    var query = from item in _cache.Contacts
                group item by item.LastName[0] into g
                orderby g.Key
                select new { GroupName = g.Key, Items = g };

    foreach (var g in query)
    {
         GroupInfoList info = new GroupInfoList();
         info.Key = g.GroupName;
         foreach (var item in g.Items)
         {
             info.Add(item);
         }
         groups.Add(info);
    }

    return groups;
}
Run Code Online (Sandbox Code Playgroud)

其中GroupInfoList定义为

public class GroupInfoList : List<object>
{
   public object Key { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这至少使我们在View中显示了一个分组集合,但是对datacache集合的更新不再实时反映.我们可以捕获datacache的CollectionChanged事件并在viewmodel中使用它来刷新GroupedContacts集合,但是这会为datacache中的每个更改创建一个新集合,导致ListView闪烁并重置选择等,这显然是次优的.

另外,动态交换分组似乎需要为每个分组场景完全单独的ObservableCollection分组项,并且要在运行时交换ListView的ItemSource绑定.

我所看到的UWP环境的其余部分似乎非常有用,所以我很惊讶地找到了像分组和排序列表一样重要的东西......

谁知道如何正确地做到这一点?

Mik*_*tly 5

我已经开始整理一个名为GroupedObservableCollection的库,它为我的一个应用程序执行这些操作.

我需要解决的一个关键问题是刷新用于创建组的原始列表,即我不希望用户搜索具有稍微不同的标准来导致整个列表被刷新,只是差异.

目前的形式,它可能不会立即回答你所有的排序问题,但对其他人来说这可能是一个很好的起点.


Nic*_*ick 1

迄今为止最好的努力使用以下辅助类 ObservableGroupingCollection

\n\n
public class ObservableGroupingCollection<K, T> where K : IComparable\n{\n    public ObservableGroupingCollection(ObservableCollection<T> collection)\n    {\n        _rootCollection = collection;\n        _rootCollection.CollectionChanged += _rootCollection_CollectionChanged;\n    }\n\n    ObservableCollection<T> _rootCollection;\n    private void _rootCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)\n    {\n        HandleCollectionChanged(e);\n    }\n\n    ObservableCollection<Grouping<K, T>> _items;\n    public ObservableCollection<Grouping<K, T>> Items\n    {\n        get { return _items; }\n    }\n\n    IComparer<T> _sortOrder;\n    Func<T, K> _groupFunction;\n\n    public void ArrangeItems(IComparer<T> sortorder, Func<T, K> group)\n    {\n        _sortOrder = sortorder;\n        _groupFunction = group;\n\n        var temp = _rootCollection\n            .OrderBy(i => i, _sortOrder)\n            .GroupBy(_groupFunction)\n            .ToList()\n            .Select(g => new Grouping<K, T>(g.Key, g));\n\n        _items = new ObservableCollection<Grouping<K, T>>(temp);\n\n    }\n\n    private void HandleCollectionChanged(NotifyCollectionChangedEventArgs e)\n    {\n        if (e.Action == NotifyCollectionChangedAction.Add)\n        {\n            var item = (T)(e.NewItems[0]);\n            var value = _groupFunction.Invoke(item);\n\n            // find matching group if exists\n            var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value));\n\n            if (existingGroup == null)\n            {\n                var newlist = new List<T>();\n                newlist.Add(item);\n\n                // find first group where Key is greater than this key\n                var insertBefore = _items.FirstOrDefault(g => ((g.Key).CompareTo(value)) > 0);\n                if (insertBefore == null)\n                {\n                    // not found - add new group to end of list\n                    _items.Add(new Grouping<K, T>(value, newlist));\n                }\n                else\n                {\n                    // insert new group at this index\n                    _items.Insert(_items.IndexOf(insertBefore), new Grouping<K, T>(value, newlist));\n                }\n            }\n            else\n            {\n                // find index to insert new item in existing group\n                int index = existingGroup.ToList().BinarySearch(item, _sortOrder);\n                if (index < 0)\n                {\n                    existingGroup.Insert(~index, item);\n                }\n            }\n        }\n        else if (e.Action == NotifyCollectionChangedAction.Remove)\n        {\n            var item = (T)(e.OldItems[0]);\n            var value = _groupFunction.Invoke(item);\n\n            var existingGroup = _items.FirstOrDefault(g => g.Key.Equals(value));\n\n            if (existingGroup != null)\n            {\n                // find existing item and remove\n                var targetIndex = existingGroup.IndexOf(item);\n                existingGroup.RemoveAt(targetIndex);\n\n                // remove group if zero items\n                if (existingGroup.Count == 0)\n                {\n                    _items.Remove(existingGroup);\n                }\n            }\n        }\n\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

其中通用 Grouping 类(它本身公开了 ObservableCollection)来自本文

\n\n

http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping

\n\n

制作工作演示:-

\n\n

从新的 UWP Blank 应用程序中,添加上述 ObservableGroupingCollection 类。然后在同一命名空间中添加另一个类文件并添加以下所有类

\n\n
// Data models\n\npublic class Contact\n{\n    public string FirstName { get; set; }\n    public string LastName { get; set; }\n    public string State { get; set; }\n}\n\npublic class DataPool\n{\n    public static string GenerateFirstName(Random random)\n    {\n        List<string> names = new List<string>() { "Lilly", "Mukhtar", "Sophie", "Femke", "Abdul-Rafi", "Mariana", "Aarif", "Sara", "Ibadah", "Fakhr", "Ilene", "Sardar", "Hanna", "Julie", "Iain", "Natalia", "Henrik", "Rasa", "Quentin", "Gadi", "Pernille", "Ishtar", "Jimmy", "Justine", "Lale", "Elize", "Randy", "Roshanara", "Rajab", "Marcus", "Mark", "Alima", "Francisco", "Thaqib", "Andreas", "Marianna", "Amalie", "Rodney", "Dena", "Amar", "Anna", "Nasreen", "Reema", "Tomas", "Filipa", "Frank", "Bari\'ah", "Parvaiz", "Jibran", "Tomas", "Elli", "Carlos", "Diego", "Henrik", "Aruna", "Vahid", "Eliana", "Roxanne", "Amanda", "Ingrid", "Wesley", "Malika", "Basim", "Eisa", "Alina", "Andreas", "Deeba", "Diya", "Parveen", "Bakr", "Celine", "Daniel", "Mattheus", "Edmee", "Hedda", "Maria", "Maja", "Alhasan", "Alina", "Hedda", "Vanja", "Robin", "Victor", "Aaftab", "Guilherme", "Maria", "Kai", "Sabien", "Abdel", "Jason", "Bahaar", "Vasco", "Jibran", "Parsa", "Catalina", "Fouad", "Colette", "John", "Fred", "James", "Harry", "Ben", "Steven", "Philip", "Dougal", "Jasper", "Elliott", "Charles", "Gerty", "Sarah", "Sonya", "Svetlana", "Dita", "Karen", "Christine", "Angela", "Heather", "Spence", "Graham", "David", "Bernie", "Darren", "Lester", "Vince", "Colin", "Bernhard", "Dieter", "Norman", "William", "Nigel", "Nick", "Nikki", "Trent", "Devon", "Steven", "Eric", "Derek", "Raymond", "Craig" };\n        return names[random.Next(0, names.Count)];\n    }\n    public static string GenerateLastName(Random random)\n    {\n        List<string> lastnames = new List<string>() { "Carlson", "Attia", "Quincey", "Hollenberg", "Khoury", "Araujo", "Hakimi", "Seegers", "Abadi", "Krommenhoek", "Siavashi", "Kvistad", "Vanderslik", "Fernandes", "Dehmas", "Sheibani", "Laamers", "Batlouni", "Lyngv\xc3\xa6r", "Oveisi", "Veenhuizen", "Gardenier", "Siavashi", "Mutlu", "Karzai", "Mousavi", "Natsheh", "Nevland", "L\xc3\xa6greid", "Bishara", "Cunha", "Hotaki", "Kyvik", "Cardoso", "Pilskog", "Pennekamp", "Nuijten", "Bettar", "Borsboom", "Skistad", "Asef", "Sayegh", "Sousa", "Miyamoto", "Medeiros", "Kregel", "Shamoun", "Behzadi", "Kuzbari", "Ferreira", "Barros", "Fernandes", "Xuan", "Formosa", "Nolette", "Shahrestaani", "Correla", "Amiri", "Sousa", "Fretheim", "Van", "Hamade", "Baba", "Mustafa", "Bishara", "Formo", "Hemmati", "Nader", "Hatami", "Natsheh", "Langen", "Maloof", "Patel", "Berger", "Ostrem", "Bardsen", "Kramer", "Bekken", "Salcedo", "Holter", "Nader", "Bettar", "Georgsen", "Cuninho", "Zardooz", "Araujo", "Batalha", "Antunes", "Vanderhoorn", "Srivastava", "Trotter", "Siavashi", "Montes", "Sherzai", "Vanderschans", "Neves", "Sarraf", "Kuiters", "Hestoe", "Cornwall", "Paisley", "Cooper", "Jakoby", "Smith", "Davies", "Jonas", "Bowers", "Fernandez", "Perez", "Black", "White", "Keller", "Hernandes", "Clinton", "Merryweather", "Freeman", "Anguillar", "Goodman", "Hardcastle", "Emmott", "Kirkby", "Thatcher", "Jamieson", "Spender", "Harte", "Pinkman", "Winterman", "Knight", "Taylor", "Wentworth", "Manners", "Walker", "McPherson", "Elder", "McDonald", "Macintosh", "Decker", "Takahashi", "Wagoner" };\n        return lastnames[random.Next(0, lastnames.Count)];\n    }\n    public static string GenerateState(Random random)\n    {\n        List<string> states = new List<string>() { "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District Of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming" };\n        return states[random.Next(0, states.Count)];\n    }\n}\n\npublic class Cache\n{\n    public Cache()\n    {\n        InitializeCacheData();\n        SimulateLiveChanges(new TimeSpan(0, 0, 1));\n    }\n\n    public ObservableCollection<Contact> Contacts { get; set; }\n\n    private static Random rnd = new Random();\n\n    private void InitializeCacheData()\n    {\n        Contacts = new ObservableCollection<Contact>();\n\n        var i = 0;\n        while (i < 5)\n        {\n            Contacts.Add(new Contact()\n            {\n                FirstName = DataPool.GenerateFirstName(rnd),\n                LastName = DataPool.GenerateLastName(rnd),\n                State = DataPool.GenerateState(rnd)\n            });\n\n            i++;\n        }\n    }\n\n    private async void SimulateLiveChanges(TimeSpan MyInterval)\n    {\n        double MyIntervalSeconds = MyInterval.TotalSeconds;\n        while (true)\n        {\n            await Task.Delay(MyInterval);\n\n            //int addOrRemove = rnd.Next(1, 10);\n            //if (addOrRemove > 3)\n            //{\n            // add item\n            Contacts.Add(new Contact()\n            {\n                FirstName = DataPool.GenerateFirstName(rnd),\n                LastName = DataPool.GenerateLastName(rnd),\n                State = DataPool.GenerateState(rnd)\n            });\n            //}\n            //else\n            //{\n            //    // remove random item\n            //    if (Contacts.Count > 0)\n            //    {\n            //        Contacts.RemoveAt(rnd.Next(0, Contacts.Count - 1));\n            //    }\n            //}\n        }\n    }\n\n}\n\n// ViewModel\n\npublic class ViewModel : BaseViewModel\n{       \n    public ViewModel()\n    {\n        _groupingCollection = new ObservableGroupingCollection<string, Contact>(new Cache().Contacts);\n        _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State));\n        NotifyPropertyChanged("GroupedContacts");\n\n    }\n\n    ObservableGroupingCollection<string, Contact> _groupingCollection;\n    public ObservableCollection<Grouping<string, Contact>> GroupedContacts\n    {\n        get\n        {\n            return _groupingCollection.Items;\n        }\n    }\n\n    // swap grouping commands\n\n    private ICommand _groupByStateCommand;\n    public ICommand GroupByStateCommand\n    {\n        get\n        {\n            if (_groupByStateCommand == null)\n            {\n                _groupByStateCommand = new RelayCommand(\n                    param => GroupByState(),\n                    param => true);\n            }\n            return _groupByStateCommand;\n        }\n    }\n    private void GroupByState()\n    {\n        _groupingCollection.ArrangeItems(new StateSorter(), (x => x.State));\n        NotifyPropertyChanged("GroupedContacts");\n    }\n\n    private ICommand _groupByNameCommand;\n    public ICommand GroupByNameCommand\n    {\n        get\n        {\n            if (_groupByNameCommand == null)\n            {\n                _groupByNameCommand = new RelayCommand(\n                    param => GroupByName(),\n                    param => true);\n            }\n            return _groupByNameCommand;\n        }\n    }\n    private void GroupByName()\n    {\n        _groupingCollection.ArrangeItems(new NameSorter(), (x => x.LastName.First().ToString()));\n        NotifyPropertyChanged("GroupedContacts");\n    }\n\n}\n\n// View Model helpers\n\npublic class BaseViewModel : INotifyPropertyChanged\n{\n    public event PropertyChangedEventHandler PropertyChanged;\n\n    protected virtual void NotifyPropertyChanged([CallerMemberName] string propertyName = null)\n    {\n        if (PropertyChanged != null)\n        {\n            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));\n        }\n    }\n}\n\npublic class RelayCommand : ICommand\n{\n    readonly Action<object> _execute;\n    readonly Predicate<object> _canExecute;\n\n    public RelayCommand(Action<object> execute)\n        : this(execute, null)\n    {\n\n    }\n\n    public RelayCommand(Action<object> execute, Predicate<object> canExecute)\n    {\n        if (execute == null)\n            throw new ArgumentNullException("execute");\n\n        _execute = execute;\n        _canExecute = canExecute;\n\n    }\n\n    public bool CanExecute(object parameter)\n    {\n        return _canExecute == null ? true : _canExecute(parameter);\n    }\n\n    public event EventHandler CanExecuteChanged\n    {\n        add { } \n        remove { } \n    }\n\n    public void Execute(object parameter)\n    {\n        _execute(parameter);\n    }\n\n}\n\n// Sorter classes\n\npublic class NameSorter : Comparer<Contact>\n{\n    public override int Compare(Contact x, Contact y)\n    {\n        int result = x.LastName.First().CompareTo(y.LastName.First());\n\n        if (result != 0)\n        {\n            return result;\n        }\n        else\n        {\n            result = x.LastName.CompareTo(y.LastName);\n\n            if (result != 0)\n            {\n                return result;\n            }\n            else\n            {\n                return x.FirstName.CompareTo(y.FirstName);\n            }\n        }\n    }\n}\n\npublic class StateSorter : Comparer<Contact>\n{\n    public override int Compare(Contact x, Contact y)\n    {\n        int result = x.State.CompareTo(y.State);\n\n        if (result != 0)\n        {\n            return result;\n        }\n        else\n        {\n            result = x.LastName.CompareTo(y.LastName);\n\n            if (result != 0)\n            {\n                return result;\n            }\n            else\n            {\n                return x.FirstName.CompareTo(y.FirstName);\n            }\n        }\n    }\n}\n\n// Grouping class \n// credit\n// http://motzcod.es/post/94643411707/enhancing-xamarinforms-listview-with-grouping\n\npublic class Grouping<K, T> : ObservableCollection<T>\n{\n    public K Key { get; private set; }\n\n    public Grouping(K key, IEnumerable<T> items)\n    {\n        Key = key;\n        foreach (var item in items)\n        {\n            this.Items.Add(item);\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后编辑MainPage如下

\n\n
  <Page.DataContext>\n        <local:ViewModel />\n    </Page.DataContext>\n\n    <Page.Resources>\n        <CollectionViewSource \n            x:Key="cvs" \n            Source="{Binding GroupedContacts}" \n            IsSourceGrouped="True" />\n    </Page.Resources>\n\n    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">\n\n        <Grid.RowDefinitions>\n            <RowDefinition Height="*" />\n            <RowDefinition Height="Auto" />\n        </Grid.RowDefinitions>\n\n        <ListView ItemsSource="{Binding Source={StaticResource cvs}}"\n                  x:Name="targetListBox">\n            <ListView.ItemTemplate>\n                <DataTemplate>\n                    <Grid>\n                        <Grid.ColumnDefinitions>\n                            <ColumnDefinition Width="100" />\n                            <ColumnDefinition Width="100" />\n                            <ColumnDefinition Width="*" />\n                        </Grid.ColumnDefinitions>\n\n                        <TextBlock Text="{Binding LastName}" />\n                        <TextBlock Text="{Binding FirstName}" Grid.Column="1" />\n                        <TextBlock Text="{Binding State}" Grid.Column="2" HorizontalAlignment="Right" />\n                    </Grid>\n                </DataTemplate>\n            </ListView.ItemTemplate>\n            <ListView.GroupStyle>\n                <GroupStyle>\n                    <GroupStyle.HeaderTemplate>\n                        <DataTemplate>\n                            <Grid Background="Gainsboro">\n                                <TextBlock FontWeight="Bold" \n                                           FontSize="14" \n                                           Margin="10,2"\n                                           Text="{Binding Key}"/>\n                            </Grid>\n                        </DataTemplate>\n                    </GroupStyle.HeaderTemplate>\n                </GroupStyle>\n            </ListView.GroupStyle>\n        </ListView>\n\n        <StackPanel Orientation="Horizontal" Grid.Row="1">\n            <Button Content="Group By Initial" Command="{Binding GroupByNameCommand}" />\n            <Button Content="Group By State" Command="{Binding GroupByStateCommand}" />\n        </StackPanel>\n    </Grid>\n
Run Code Online (Sandbox Code Playgroud)\n\n

到目前为止,HandleCollectionChanged 方法仅处理添加/删除,如果 NotifyCollectionChangedEventArgs 参数包含多个项目,该方法将崩溃(现有的 ObservableCollection 类一次仅通知一项更改)

\n\n

所以它工作正常,但感觉有点老套。

\n\n

非常欢迎提出改进建议。

\n